Skip to content

Commit 326c635

Browse files
committed
feat: use vercel api gateway
1 parent a204066 commit 326c635

4 files changed

Lines changed: 93 additions & 74 deletions

File tree

examples/ai-conference-assistant/package-lock.json

Lines changed: 0 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/ai-conference-assistant/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
},
1111
"dependencies": {
1212
"@hackmd/api": "file:../../nodejs",
13-
"@ai-sdk/openai": "^3.0.48",
1413
"@ai-sdk/react": "^3.0.140",
1514
"ai": "^6.0.138",
1615
"next": "16.2.1",

examples/ai-conference-assistant/src/app/api/chat/route.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* Note creation is handled separately via /api/create-notes.
77
*/
88

9-
import { streamText, stepCountIs, type ModelMessage } from 'ai'
10-
import { createOpenAI } from '@ai-sdk/openai'
9+
import { convertToModelMessages, createGateway, streamText, stepCountIs, type UIMessage } from 'ai'
1110
import { createTools } from '@/lib/tools'
1211

1312
export const maxDuration = 60
@@ -24,12 +23,15 @@ You have tools to:
2423
2524
## Workflow
2625
1. First, if the user hasn't verified their setup, call hackmd_get_me to check credentials
27-
2. Ask about: conference name, team path, session data (user uploads JSON)
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.
2827
3. Use jq_query to analyze the session data shape and summarize it for the user
2928
4. If user mentions a reference note, fetch it with hackmd_get_note
3029
5. Use generate_pages to create all pages, show preview
3130
6. User confirms → they click "Create Notes" button in the UI
3231
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+
3335
## Important Notes
3436
- Always use jq_query first to understand data shape before generating pages — this saves tokens
3537
- When showing previews, show the homepage and 1-2 sample session pages
@@ -55,13 +57,15 @@ But you should use jq_query to discover the actual shape of uploaded data and ad
5557

5658
export async function POST(req: Request) {
5759
const body = await req.json()
58-
const { messages, config } = body as {
59-
messages: ModelMessage[]
60+
const { messages, config, sessionDataJson } = body as {
61+
messages: UIMessage[]
6062
config: {
6163
apiKey: string
6264
apiEndpoint: string
6365
teamPath: string
6466
}
67+
/** Raw session JSON; sent out-of-band so the chat UI does not embed huge payloads. */
68+
sessionDataJson?: string
6569
}
6670

6771
if (!config?.apiKey) {
@@ -85,16 +89,29 @@ export async function POST(req: Request) {
8589
}
8690

8791
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()) {
97+
try {
98+
const parsed = JSON.parse(sessionDataJson) as unknown
99+
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>`
101+
} 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>`
103+
}
104+
}
88105

89-
const openai = createOpenAI({
106+
const gateway = createGateway({
90107
apiKey: aiGatewayApiKey,
91108
...(process.env.AI_GATEWAY_BASE_URL && { baseURL: process.env.AI_GATEWAY_BASE_URL }),
92109
})
93110

94111
const result = streamText({
95-
model: openai('gpt-4o'),
96-
system: SYSTEM_PROMPT,
97-
messages,
112+
model: gateway('openai/gpt-5.4-mini'),
113+
system,
114+
messages: modelMessages,
98115
tools,
99116
stopWhen: stepCountIs(10),
100117
})

examples/ai-conference-assistant/src/components/chat-panel.tsx

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@
22

33
import { useChat } from '@ai-sdk/react'
44
import { TextStreamChatTransport } from 'ai'
5-
import { useState, useRef, useEffect, type MutableRefObject, type FormEvent } from 'react'
5+
import {
6+
useState,
7+
useRef,
8+
useEffect,
9+
useMemo,
10+
type MutableRefObject,
11+
type FormEvent,
12+
type KeyboardEvent,
13+
} from 'react'
614
import type { AppConfig, GeneratedData } from '@/app/page'
715

8-
function safeSessionArrayLength(json: string): number {
9-
const t = json.trim()
10-
if (!t) return 0
11-
try {
12-
const parsed = JSON.parse(t) as unknown
13-
return Array.isArray(parsed) ? parsed.length : 0
14-
} catch {
15-
return 0
16-
}
17-
}
18-
1916
interface ChatPanelProps {
2017
config: AppConfig
2118
sessionData: MutableRefObject<string>
@@ -38,22 +35,47 @@ export function ChatPanel({
3835
const [fileUploaded, setFileUploaded] = useState(false)
3936
/** Set when a file parses successfully; avoids JSON.parse on ref before parent syncs sessionDataRef. */
4037
const [uploadedSessionCount, setUploadedSessionCount] = useState<number | null>(null)
41-
const [sessionDataSent, setSessionDataSent] = useState(false)
4238
const [inputValue, setInputValue] = useState('')
4339
const messagesEndRef = useRef<HTMLDivElement>(null)
4440
const fileInputRef = useRef<HTMLInputElement>(null)
41+
/** Same JSON as parent ref, set synchronously on file read so the first send cannot race useEffect. */
42+
const localSessionJsonRef = useRef('')
43+
const fileUploadedRef = useRef(false)
44+
useEffect(() => {
45+
fileUploadedRef.current = fileUploaded
46+
}, [fileUploaded])
4547

46-
const { messages, sendMessage, status, error } = useChat({
47-
transport: new TextStreamChatTransport({
48-
api: '/api/chat',
49-
body: {
50-
config: {
51-
apiKey: config.apiKey,
52-
apiEndpoint: config.apiEndpoint,
53-
teamPath: config.teamPath,
48+
const transport = useMemo(
49+
() =>
50+
new TextStreamChatTransport({
51+
api: '/api/chat',
52+
body: {
53+
config: {
54+
apiKey: config.apiKey,
55+
apiEndpoint: config.apiEndpoint,
56+
teamPath: config.teamPath,
57+
},
5458
},
55-
},
56-
}),
59+
prepareSendMessagesRequest: ({ body, messages }) => {
60+
// `body` is only the static `config` from transport init; `messages` is passed separately and must be merged in.
61+
const merged: Record<string, unknown> = {
62+
...(body as Record<string, unknown>),
63+
messages,
64+
}
65+
const json =
66+
localSessionJsonRef.current.trim() || sessionData.current.trim()
67+
// Include on every send while a file is loaded so the model keeps <session_data> in context.
68+
if (fileUploadedRef.current && json) {
69+
merged.sessionDataJson = json
70+
}
71+
return { body: merged }
72+
},
73+
}),
74+
[config.apiKey, config.apiEndpoint, config.teamPath],
75+
)
76+
77+
const { messages, sendMessage, status, error } = useChat({
78+
transport,
5779
})
5880

5981
const isLoading = status === 'submitted' || status === 'streaming'
@@ -73,6 +95,7 @@ export function ChatPanel({
7395
try {
7496
const parsed = JSON.parse(text) as unknown
7597
const count = Array.isArray(parsed) ? parsed.length : 0
98+
localSessionJsonRef.current = text
7699
onSessionDataChange(text)
77100
setUploadedSessionCount(count)
78101
setFileUploaded(true)
@@ -83,26 +106,22 @@ export function ChatPanel({
83106
reader.readAsText(file)
84107
}
85108

109+
function sendCurrentMessage() {
110+
if (!inputValue.trim() || isLoading) return
111+
void sendMessage({ text: inputValue })
112+
setInputValue('')
113+
}
114+
86115
function onSubmit(e: FormEvent) {
87116
e.preventDefault()
88-
if (!inputValue.trim() || isLoading) return
117+
sendCurrentMessage()
118+
}
89119

90-
// If session data is available and hasn't been sent yet, include it
91-
let text = inputValue
92-
if (
93-
fileUploaded &&
94-
sessionData.current.trim() &&
95-
!sessionDataSent
96-
) {
97-
const count =
98-
uploadedSessionCount ??
99-
safeSessionArrayLength(sessionData.current)
100-
text = `${inputValue}\n\n[Session data uploaded - ${count} sessions]\n<session_data>\n${sessionData.current}\n</session_data>`
101-
setSessionDataSent(true)
120+
function onComposerKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
121+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
122+
e.preventDefault()
123+
sendCurrentMessage()
102124
}
103-
104-
sendMessage({ text })
105-
setInputValue('')
106125
}
107126

108127
return (
@@ -270,7 +289,7 @@ export function ChatPanel({
270289

271290
{/* Input area */}
272291
<div className="px-6 py-4 bg-white border-t border-gray-200">
273-
<form onSubmit={onSubmit} className="flex gap-3">
292+
<form onSubmit={onSubmit} className="flex gap-3 items-end">
274293
<input
275294
ref={fileInputRef}
276295
type="file"
@@ -281,26 +300,27 @@ export function ChatPanel({
281300
<button
282301
type="button"
283302
onClick={() => fileInputRef.current?.click()}
284-
className="px-3 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition text-gray-600"
303+
className="px-3 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition text-gray-600 shrink-0 self-end"
285304
title="Upload sessions JSON"
286305
>
287306
📁
288307
</button>
289-
<input
290-
type="text"
308+
<textarea
291309
value={inputValue}
292310
onChange={e => setInputValue(e.target.value)}
311+
onKeyDown={onComposerKeyDown}
312+
rows={3}
293313
placeholder={
294314
fileUploaded
295-
? 'Tell me about your conference...'
296-
: 'Upload session data first, or ask me anything...'
315+
? 'Message… (Shift+Enter for new line, Enter to send)'
316+
: 'Upload session data or ask anything… (Shift+Enter for new line)'
297317
}
298-
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-gray-900"
318+
className="flex-1 min-h-[2.75rem] max-h-40 px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-gray-900 resize-y"
299319
/>
300320
<button
301321
type="submit"
302322
disabled={isLoading || !inputValue.trim()}
303-
className="px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition"
323+
className="px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition shrink-0"
304324
>
305325
Send
306326
</button>

0 commit comments

Comments
 (0)