Add session export/import as JSON
Sessions can now be downloaded as a JSON file containing the session
metadata and every message with its tags, and re-imported as a new
session from that same format.
GET /chat/sessions/{id}/export streams the session as a file download.
POST /chat/sessions/import validates the roles, then recreates the
session under a fresh UUID so importing never collides with an existing
session. The sidebar gets a per-row download button and an import button
that feeds a hidden file input.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useChatStore } from '../stores/chat'
|
||||
|
||||
const store = useChatStore()
|
||||
const error = ref<string | null>(null)
|
||||
const importInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
async function load() {
|
||||
error.value = null
|
||||
@@ -14,6 +15,18 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function triggerImport() {
|
||||
importInput.value?.click()
|
||||
}
|
||||
|
||||
async function onImportFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
await store.importSessionFromFile(file)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -21,7 +34,17 @@ onMounted(load)
|
||||
<aside class="session-list">
|
||||
<div class="header">
|
||||
<span>チャット履歴</span>
|
||||
<button class="new-btn" @click="store.selectSession(null)">+ 新規</button>
|
||||
<div class="header-actions">
|
||||
<button class="import-btn" title="JSONからインポート" @click="triggerImport">⭱</button>
|
||||
<button class="new-btn" @click="store.selectSession(null)">+ 新規</button>
|
||||
</div>
|
||||
<input
|
||||
ref="importInput"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
class="hidden-file-input"
|
||||
@change="onImportFileChange"
|
||||
>
|
||||
</div>
|
||||
<div v-if="error" class="api-error" :title="error">⚠ 接続エラー</div>
|
||||
<ul>
|
||||
@@ -32,6 +55,11 @@ onMounted(load)
|
||||
@click="store.selectSession(s.id)"
|
||||
>
|
||||
<span class="title">{{ s.title }}</span>
|
||||
<button
|
||||
class="export-btn"
|
||||
@click.stop="store.downloadSessionLog(s.id)"
|
||||
title="JSONでエクスポート"
|
||||
>⭳</button>
|
||||
<button
|
||||
class="del-btn"
|
||||
@click.stop="store.removeSession(s.id)"
|
||||
@@ -69,7 +97,13 @@ onMounted(load)
|
||||
border-bottom: 1px solid #7f1d1d;
|
||||
cursor: default;
|
||||
}
|
||||
.new-btn {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.new-btn,
|
||||
.import-btn {
|
||||
background: #2a2a2a;
|
||||
color: #ccc;
|
||||
border: 1px solid #444;
|
||||
@@ -78,6 +112,9 @@ onMounted(load)
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -104,6 +141,7 @@ li.active { background: #333; }
|
||||
font-size: 13px;
|
||||
color: #ddd;
|
||||
}
|
||||
.export-btn,
|
||||
.del-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -114,5 +152,7 @@ li.active { background: #333; }
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.export-btn { font-size: 13px; }
|
||||
li:hover .export-btn,
|
||||
li:hover .del-btn { opacity: 1; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user