Add shared notes with tabbed pages editable by user and AI
Adds a note_pages table backing a tabbed notes panel where the user and the model can both read and write. Only page titles (plus a short preview) are injected into the system prompt on every turn; full page content is kept out of context and fetched on demand via a read_note tool, with write_note letting the model append to, replace, or create a page. Generalizes llm.resolve_web_search into resolve_tool_calls so the tool-calling loop handles web_search and the new note tools through one path. The chat stream emits a notes_updated SSE event when the model edits a note so the frontend panel can refresh; local edits are tracked as dirty and never clobbered by that refresh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useNotesStore } from './notes'
|
||||
import { ref, computed } from 'vue'
|
||||
import { fetchSessions, fetchMessages, deleteSession, exportSession, importSession, streamChat, type SessionInfo, type Source, type ImportPayload } from '../api/client'
|
||||
|
||||
@@ -115,6 +116,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
messages.value[msgIdx].content += chunk.value as string
|
||||
} else if (chunk.type === 'sources' && chunk.value) {
|
||||
messages.value[msgIdx].sources = chunk.value as Source[]
|
||||
} else if (chunk.type === 'notes_updated') {
|
||||
// AIが共有ノートを書き換えたので、開いているノートパネルに反映する
|
||||
useNotesStore().load().catch((e) => pushToast(`ノート更新失敗: ${e.message}`))
|
||||
} else if (chunk.type === 'done') {
|
||||
messages.value[msgIdx].pending = false
|
||||
isStreaming.value = false
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
fetchNotePages,
|
||||
createNotePage,
|
||||
updateNotePage,
|
||||
deleteNotePage,
|
||||
type NotePage,
|
||||
} from '../api/client'
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 800
|
||||
|
||||
export const useNotesStore = defineStore('notes', () => {
|
||||
const pages = ref<NotePage[]>([])
|
||||
const activeId = ref<number | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
/** 未保存の編集があるページID。AIによる更新で上書きしないために使う。 */
|
||||
const dirtyIds = ref<Set<number>>(new Set())
|
||||
|
||||
let saveTimer: number | null = null
|
||||
|
||||
const activePage = computed(() => pages.value.find((p) => p.id === activeId.value) ?? null)
|
||||
|
||||
async function load(): Promise<void> {
|
||||
error.value = null
|
||||
try {
|
||||
const fetched = await fetchNotePages()
|
||||
// 編集中のページはローカルの内容を優先し、サーバーの値で潰さない
|
||||
pages.value = fetched.map((page) => {
|
||||
if (!dirtyIds.value.has(page.id)) return page
|
||||
const local = pages.value.find((p) => p.id === page.id)
|
||||
return local ? { ...page, title: local.title, content: local.content } : page
|
||||
})
|
||||
if (activeId.value === null || !pages.value.some((p) => p.id === activeId.value)) {
|
||||
activeId.value = pages.value[0]?.id ?? null
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ノートの取得に失敗しました'
|
||||
}
|
||||
}
|
||||
|
||||
function selectPage(id: number): void {
|
||||
flush()
|
||||
activeId.value = id
|
||||
}
|
||||
|
||||
async function addPage(title = '新しいページ'): Promise<void> {
|
||||
error.value = null
|
||||
try {
|
||||
const page = await createNotePage(title)
|
||||
pages.value.push(page)
|
||||
activeId.value = page.id
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ページの作成に失敗しました'
|
||||
}
|
||||
}
|
||||
|
||||
async function removePage(id: number): Promise<void> {
|
||||
error.value = null
|
||||
try {
|
||||
await deleteNotePage(id)
|
||||
dirtyIds.value.delete(id)
|
||||
pages.value = pages.value.filter((p) => p.id !== id)
|
||||
if (activeId.value === id) activeId.value = pages.value[0]?.id ?? null
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ページの削除に失敗しました'
|
||||
}
|
||||
}
|
||||
|
||||
/** ローカルの内容を即座に更新し、少し待ってからサーバーへ保存する。 */
|
||||
function edit(id: number, patch: { title?: string; content?: string }): void {
|
||||
const page = pages.value.find((p) => p.id === id)
|
||||
if (!page) return
|
||||
Object.assign(page, patch)
|
||||
dirtyIds.value.add(id)
|
||||
|
||||
if (saveTimer !== null) clearTimeout(saveTimer)
|
||||
saveTimer = window.setTimeout(() => {
|
||||
saveTimer = null
|
||||
void save(id)
|
||||
}, SAVE_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
async function save(id: number): Promise<void> {
|
||||
const page = pages.value.find((p) => p.id === id)
|
||||
if (!page || !dirtyIds.value.has(id)) return
|
||||
const { title, content } = page
|
||||
try {
|
||||
const saved = await updateNotePage(id, { title, content })
|
||||
// 保存の往復中にさらに編集されていた場合は dirty のままにしておく
|
||||
if (page.title === title && page.content === content) {
|
||||
dirtyIds.value.delete(id)
|
||||
page.updated_at = saved.updated_at
|
||||
}
|
||||
error.value = null
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ノートの保存に失敗しました'
|
||||
}
|
||||
}
|
||||
|
||||
/** 保留中の保存を直ちに実行する(タブ切替・パネルを閉じる時など)。 */
|
||||
function flush(): void {
|
||||
if (saveTimer !== null) {
|
||||
clearTimeout(saveTimer)
|
||||
saveTimer = null
|
||||
}
|
||||
for (const id of [...dirtyIds.value]) void save(id)
|
||||
}
|
||||
|
||||
return {
|
||||
pages,
|
||||
activeId,
|
||||
activePage,
|
||||
error,
|
||||
dirtyIds,
|
||||
load,
|
||||
selectPage,
|
||||
addPage,
|
||||
removePage,
|
||||
edit,
|
||||
flush,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user