Skip to content

Commit 991b5fa

Browse files
committed
feat: frontend tool calling
1 parent 326c635 commit 991b5fa

5 files changed

Lines changed: 284 additions & 131 deletions

File tree

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,41 @@
11
/**
22
* AI Chat API Route
33
*
4-
* Handles streaming chat with the AI agent. The agent has access to tools
5-
* for reading HackMD notes, querying session data, and generating pages.
6-
* Note creation is handled separately via /api/create-notes.
4+
* Session JSON is passed only into tool context (session_jq, generate_preview_pages),
5+
* not embedded in the system prompt. The model analyzes shape via session_jq first.
76
*/
87

98
import { convertToModelMessages, createGateway, streamText, stepCountIs, type UIMessage } from 'ai'
109
import { createTools } from '@/lib/tools'
1110

1211
export const maxDuration = 60
1312

14-
const SYSTEM_PROMPT = `You are a HackMD Conference Note Assistant (共筆小幫手). You help users create book-mode collaborative note systems for conferences.
15-
16-
## Your Capabilities
17-
You have tools to:
18-
1. **hackmd_get_me** — Verify API credentials and discover available teams
19-
2. **hackmd_get_note** — Read existing notes for reference/templates
20-
3. **hackmd_get_team_notes** — List notes in a team workspace
21-
4. **jq_query** — Analyze session data efficiently (counts, grouping, field extraction)
22-
5. **generate_pages** — Generate all conference note pages for preview
23-
24-
## Workflow
25-
1. First, if the user hasn't verified their setup, call hackmd_get_me to check credentials
26-
2. Ask about conference name and preferences. **Only** if there is no \`<session_data>\` block in your instructions for this request, ask them to upload session JSON in the UI. If \`<session_data>\` is present, session data is already loaded — do not ask for upload or paste.
27-
3. Use jq_query to analyze the session data shape and summarize it for the user
28-
4. If user mentions a reference note, fetch it with hackmd_get_note
29-
5. Use generate_pages to create all pages, show preview
30-
6. User confirms → they click "Create Notes" button in the UI
31-
32-
## When session data is already provided
33-
If this request includes an \`<session_data>\` section below, the user has already uploaded sessions in the app. **Do not** ask them to upload or paste JSON again. Start with jq_query or answer their question using that data.
34-
35-
## Important Notes
36-
- Always use jq_query first to understand data shape before generating pages — this saves tokens
37-
- When showing previews, show the homepage and 1-2 sample session pages
38-
- The actual note creation is handled by the frontend UI with progress tracking
39-
- Respond in the same language the user uses (Chinese or English)
40-
- Be concise but helpful
41-
42-
## Session Data Format
43-
The expected session data format follows the conference session JSON pattern:
13+
const SYSTEM_PROMPT = `You are a HackMD Conference Note Assistant (共筆小幫手). You help users design book-mode collaborative note systems for conferences.
14+
15+
## Tools (read the descriptions carefully)
16+
1. **hackmd_get_me** — Verify API credentials and list teams
17+
2. **hackmd_get_note** / **hackmd_get_team_notes** — Optional reference notes
18+
3. **session_jq** — Query the **uploaded** session JSON on the server (jq-like). **Always use this first** to understand schema: \`length\` → \`keys\` → \`first 3\` or \`map\` a few fields. Never ask the user to paste full session JSON.
19+
4. **generate_preview_pages** — Build **preview markdown only** (homepage + session pages) from server-side session data. You pass conference name, team path, options — **not** raw JSON. After preview, the user confirms in the UI; **you do not create HackMD notes**.
20+
21+
## Staged workflow
22+
1. If needed, **hackmd_get_me** to align team path with the user.
23+
2. If the app has session data loaded, **session_jq** repeatedly until you understand fields (types, time fields, rooms, speakers).
24+
3. Ask only for **conference name**, **announcement embed**, exclusions, or template preferences — not for raw JSON.
25+
4. Call **generate_preview_pages** when ready. The right panel shows markdown preview.
26+
5. Real HackMD creation happens **only** when the user confirms the preview in the UI and starts creation — not via chat tools.
27+
28+
## Rules
29+
- Do not ask users to paste or upload JSON in chat if the app already loaded a file (they use 📁 in the composer).
30+
- Prefer short tool outputs; summarize shape in natural language.
31+
- Respond in the user’s language (Chinese or English).
32+
- Be concise.
33+
34+
## Reference: typical session object shape (for your mental model — actual fields vary)
4435
\`\`\`json
45-
{
46-
"id": "session-001",
47-
"title": "Talk Title",
48-
"speaker": [{ "speaker": { "public_name": "Name" } }],
49-
"session_type": "talk",
50-
"started_at": "2025-03-15T09:00:00Z",
51-
"finished_at": "2025-03-15T09:30:00Z",
52-
"tags": ["tag1"],
53-
"classroom": { "tw_name": "教室A", "en_name": "Room A" }
54-
}
36+
{ "id": 1, "title": "…", "session_type": "talk", "started_at": "…", "speaker": [], "classroom": { "tw_name": "…" } }
5537
\`\`\`
56-
But you should use jq_query to discover the actual shape of uploaded data and adapt accordingly.`
38+
`
5739

5840
export async function POST(req: Request) {
5941
const body = await req.json()
@@ -64,7 +46,6 @@ export async function POST(req: Request) {
6446
apiEndpoint: string
6547
teamPath: string
6648
}
67-
/** Raw session JSON; sent out-of-band so the chat UI does not embed huge payloads. */
6849
sessionDataJson?: string
6950
}
7051

@@ -88,21 +69,30 @@ export async function POST(req: Request) {
8869
)
8970
}
9071

91-
const tools = createTools(config.apiKey, config.apiEndpoint)
92-
const uiMessages = Array.isArray(messages) ? messages : []
93-
const modelMessages = await convertToModelMessages(uiMessages, { tools })
94-
95-
let system = SYSTEM_PROMPT
96-
if (sessionDataJson?.trim()) {
72+
const trimmedSession = sessionDataJson?.trim()
73+
let sessionMeta = ''
74+
if (trimmedSession) {
9775
try {
98-
const parsed = JSON.parse(sessionDataJson) as unknown
76+
const parsed = JSON.parse(trimmedSession) as unknown
9977
const n = Array.isArray(parsed) ? parsed.length : 0
100-
system += `\n\n## Uploaded session data (${n} sessions) — attached by the app on every request while a file is loaded\n**You must not ask the user to upload or paste session JSON** — it is already in \`<session_data>\`. Use jq_query on this JSON. Use generate_pages with sessionsJson from this data when generating pages.\n\n<session_data>\n${sessionDataJson}\n</session_data>`
78+
sessionMeta = `\n\n## Session file in app\n${n} session record(s) are loaded on the server for **session_jq** / **generate_preview_pages** only. Raw JSON is **not** included in this prompt.`
10179
} catch {
102-
system += `\n\n## Uploaded session data — attached by the app; do not ask for upload/paste\n<session_data>\n${sessionDataJson}\n</session_data>`
80+
sessionMeta =
81+
'\n\n## Session file in app\nA session file is loaded (parse warning). Use **session_jq** to inspect.'
10382
}
83+
} else {
84+
sessionMeta =
85+
'\n\n## Session file\nNo session file is loaded yet. Ask the user to upload **sessions.json** via 📁 in the chat composer before analysis or preview.'
10486
}
10587

88+
const tools = createTools(config.apiKey, config.apiEndpoint, {
89+
sessionDataJson: trimmedSession || null,
90+
})
91+
const uiMessages = Array.isArray(messages) ? messages : []
92+
const modelMessages = await convertToModelMessages(uiMessages, { tools })
93+
94+
const system = SYSTEM_PROMPT + sessionMeta
95+
10696
const gateway = createGateway({
10797
apiKey: aiGatewayApiKey,
10898
...(process.env.AI_GATEWAY_BASE_URL && { baseURL: process.env.AI_GATEWAY_BASE_URL }),
@@ -113,8 +103,10 @@ export async function POST(req: Request) {
113103
system,
114104
messages: modelMessages,
115105
tools,
116-
stopWhen: stepCountIs(10),
106+
stopWhen: stepCountIs(15),
117107
})
118108

119-
return result.toTextStreamResponse()
109+
// `toTextStreamResponse()` strips non-text events — tool calls/results never reach the client,
110+
// so preview stays blank. UI message stream is required for tool parts in `useChat`.
111+
return result.toUIMessageStreamResponse()
120112
}

examples/ai-conference-assistant/src/app/page.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,25 @@ export default function Home() {
2222
const [config, setConfig] = useState<AppConfig | null>(null)
2323
const [sessionData, setSessionData] = useState<string>('')
2424
const [generatedData, setGeneratedData] = useState<GeneratedData | null>(null)
25+
/** User explicitly confirms the preview before HackMD creation is allowed. */
26+
const [previewConfirmed, setPreviewConfirmed] = useState(false)
2527
const [showProgress, setShowProgress] = useState(false)
2628
const [previewPage, setPreviewPage] = useState<{ title: string; content: string } | null>(null)
2729
const sessionDataRef = useRef(sessionData)
2830
useEffect(() => { sessionDataRef.current = sessionData }, [sessionData])
2931

32+
useEffect(() => {
33+
setPreviewConfirmed(false)
34+
}, [generatedData])
35+
3036
if (!config) {
3137
return <SetupPanel onConfigured={setConfig} />
3238
}
3339

3440
return (
35-
<div className="flex h-screen bg-gray-50">
36-
{/* Left: Chat Panel */}
37-
<div className="flex-1 flex flex-col min-w-0">
41+
<div className="flex flex-col lg:flex-row h-screen bg-gray-50 min-h-0">
42+
{/* Chat */}
43+
<div className="flex-1 flex flex-col min-h-0 min-w-0 lg:max-w-[calc(100%-480px)]">
3844
<ChatPanel
3945
config={config}
4046
sessionData={sessionDataRef}
@@ -43,15 +49,19 @@ export default function Home() {
4349
onCreateNotes={() => setShowProgress(true)}
4450
onPreviewPage={setPreviewPage}
4551
generatedData={generatedData}
52+
previewConfirmed={previewConfirmed}
4653
/>
4754
</div>
4855

49-
{/* Right: Preview Panel */}
50-
<div className="w-[480px] border-l border-gray-200 bg-white flex-shrink-0 hidden lg:flex flex-col">
56+
{/* Preview + confirm + create (always mounted so narrow screens can confirm before HackMD) */}
57+
<div className="h-[min(42vh,420px)] lg:h-auto lg:w-[480px] border-t lg:border-t-0 lg:border-l border-gray-200 bg-white flex-shrink-0 flex flex-col min-h-0">
5158
<PreviewPanel
5259
generatedData={generatedData}
5360
previewPage={previewPage}
5461
onSelectPage={setPreviewPage}
62+
previewConfirmed={previewConfirmed}
63+
onPreviewConfirmedChange={setPreviewConfirmed}
64+
onCreateNotes={() => setShowProgress(true)}
5565
/>
5666
</div>
5767

0 commit comments

Comments
 (0)