Pin notes to a chat, and import notes from files

Two additions to the shared notes, both about getting content in and out of
the prompt.

Per-chat pinning. Notes normally reach the model as a title list only, and
the body arrives just if the model decides to call read_note. A 📌 on the tab
now puts a page's full body in the system prompt for the chat that is open,
every turn, no tool round trip. The selection is per session (new table
session_note_pages, unique on session_id + page_id), so different
conversations carry different pages. The title list marks pinned pages so the
model does not read them twice, and bodies are capped per page by
SESSION_NOTES_CONTENT_LIMIT.

A new chat has no session row until its first message, so the front end
cannot PUT the selection yet. POST /chat/stream therefore accepts
note_page_ids and applies it before building the context, which makes the
pins count from the very first turn.

File import. Dropping .md/.txt on the editor - or picking them with the new
⬆ button - creates pages, and a .zip is unpacked into one page per text
entry with the first path component as the section. That is exactly the
shape the export writes, so a downloaded zip round-trips back in. Titles that
collide get a " (2)" suffix rather than overwriting, since restoring a backup
into a live workspace should not destroy anything. UploadFile needs
python-multipart, added to the lockfile.

Toasts grew a kind: the import success message was rendering in the red error
toast, as was the existing session-import message. Both are green now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 16:04:41 +09:00
co-authored by Claude Opus 5
parent 216d38c7bd
commit 0e58a07af4
18 changed files with 629 additions and 33 deletions
+20 -2
View File
@@ -3,12 +3,12 @@ import zipfile
from datetime import datetime, timezone
from urllib.parse import quote
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import Response
from pydantic import BaseModel
from sqlalchemy.orm import Session as DBSession
from app.core.notes import next_position, reorder, rename_section, set_section
from app.core.notes import import_files, next_position, reorder, rename_section, set_section
from app.db.database import get_db
from app.db.models import NotePage
@@ -129,6 +129,24 @@ def create_page(req: NotePageCreate, db: DBSession = Depends(get_db)):
return page
@router.post("/import", response_model=list[NotePageResponse], status_code=201)
async def import_pages(files: list[UploadFile] = File(...), db: DBSession = Depends(get_db)):
"""アップロードされたファイルをページとして取り込む。
.md / .txt はそのまま1ページ、.zip は中のテキストファイルを1つずつページにする
(ZIP内のフォルダ名がセクションになるので、エクスポートしたZIPをそのまま戻せる)。
対象外の拡張子や大きすぎるファイルは黙って飛ばすので、1つも取り込めなければ 400。
"""
payload = [(f.filename or "無題", await f.read()) for f in files]
created = import_files(db, payload)
if not created:
raise HTTPException(
status_code=400,
detail="取り込めるファイルがありませんでした(.md / .txt / .zip のみ)",
)
return created
@router.post("/sections/rename")
def rename_section_endpoint(req: SectionRenameRequest, db: DBSession = Depends(get_db)):
"""old_name セクションに属する全ページを new_name セクションへ一括で付け替える。new_name が空ならグループ解除。"""