Skip to content

Commit a204066

Browse files
committed
feat: use api-client and rework flow
1 parent 634c3b3 commit a204066

15 files changed

Lines changed: 207 additions & 200 deletions

File tree

examples/ai-conference-assistant/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A web-based AI assistant that helps you create book-mode collaborative note syst
99
- **Reference Note Fetching**: Point the AI to an existing HackMD note (e.g., last year's conference) and it will analyze the format
1010
- **Markdown Preview**: Preview the generated homepage and all session pages before creating
1111
- **Rate-Limit-Aware Creation**: Batch note creation with configurable delay and real-time progress tracking via SSE
12-
- **Frontend API Key Entry**: No server-side secrets needed — provide your HackMD and OpenAI API keys from the browser
12+
- **Server-side LLM credentials**: `AI_GATEWAY_API_KEY` is read only on the server (never sent from the browser); you still enter your HackMD token in the UI
1313

1414
## Architecture
1515

@@ -19,7 +19,7 @@ A web-based AI assistant that helps you create book-mode collaborative note syst
1919
│ │
2020
│ ┌──────────────┐ ┌──────────┐ ┌───────────┐ │
2121
│ │ Setup Panel │ │ Chat UI │ │ Preview │ │
22-
│ │ (API keys) │ │ (useChat)│ │ (Markdown)│ │
22+
│ │ (HackMD) │ │ (useChat)│ │ (Markdown)│ │
2323
│ └──────┬───────┘ └────┬─────┘ └───────────┘ │
2424
│ │ │ │
2525
└─────────┼───────────────┼────────────────────────┘
@@ -44,7 +44,7 @@ A web-based AI assistant that helps you create book-mode collaborative note syst
4444

4545
- Node.js 18+
4646
- A [HackMD API token](https://hackmd.io/settings/api)
47-
- An [OpenAI API key](https://platform.openai.com/api-keys)
47+
- A [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) API key (or another OpenAI-compatible key) set as **`AI_GATEWAY_API_KEY` in the server environment** — for example in `.env.local` when developing, or in your host’s env for production
4848

4949
### Setup
5050

@@ -55,6 +55,11 @@ cd examples/ai-conference-assistant
5555
# Install dependencies
5656
npm install
5757

58+
# Required: LLM key for the API route (not committed — use .env.local)
59+
echo 'AI_GATEWAY_API_KEY=your_key_here' >> .env.local
60+
# Optional: custom OpenAI-compatible base URL (e.g. Vercel AI Gateway)
61+
# echo 'AI_GATEWAY_BASE_URL=https://ai-gateway.vercel.sh/v1' >> .env.local
62+
5863
# Start the development server
5964
npm run dev
6065
```
@@ -63,7 +68,7 @@ Open [http://localhost:3000](http://localhost:3000) in your browser.
6368

6469
### Usage
6570

66-
1. **Enter your credentials**HackMD API key, OpenAI API key, and team path
71+
1. **Enter your HackMD credentials** — API key and team path (LLM access uses `AI_GATEWAY_API_KEY` on the server only)
6772
2. **Upload session data** — Click the 📁 button to upload your `sessions.json`
6873
3. **Chat with the AI** — Tell it about your conference, reference notes, customizations
6974
4. **Preview** — The AI generates pages; preview them in the right panel
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
/** Pre-bundle issues with the linked file:../../nodejs package under Turbopack; webpack resolves it reliably. */
4+
serverExternalPackages: ["@hackmd/api", "axios"],
5+
};
6+
7+
export default nextConfig;

examples/ai-conference-assistant/next.config.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

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

Lines changed: 46 additions & 0 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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
7-
"build": "next build",
6+
"dev": "next dev --webpack",
7+
"build": "next build --webpack",
88
"start": "next start",
99
"lint": "eslint"
1010
},
1111
"dependencies": {
12+
"@hackmd/api": "file:../../nodejs",
1213
"@ai-sdk/openai": "^3.0.48",
1314
"@ai-sdk/react": "^3.0.140",
1415
"ai": "^6.0.138",

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export async function POST(req: Request) {
6161
apiKey: string
6262
apiEndpoint: string
6363
teamPath: string
64-
openaiApiKey: string
6564
}
6665
}
6766

@@ -72,16 +71,25 @@ export async function POST(req: Request) {
7271
})
7372
}
7473

75-
if (!config?.openaiApiKey) {
76-
return new Response(JSON.stringify({ error: 'OpenAI API key is required' }), {
77-
status: 400,
78-
headers: { 'Content-Type': 'application/json' },
79-
})
74+
const aiGatewayApiKey = process.env.AI_GATEWAY_API_KEY
75+
if (!aiGatewayApiKey) {
76+
return new Response(
77+
JSON.stringify({
78+
error: 'Server misconfiguration: AI_GATEWAY_API_KEY is not set',
79+
}),
80+
{
81+
status: 503,
82+
headers: { 'Content-Type': 'application/json' },
83+
},
84+
)
8085
}
8186

8287
const tools = createTools(config.apiKey, config.apiEndpoint)
8388

84-
const openai = createOpenAI({ apiKey: config.openaiApiKey })
89+
const openai = createOpenAI({
90+
apiKey: aiGatewayApiKey,
91+
...(process.env.AI_GATEWAY_BASE_URL && { baseURL: process.env.AI_GATEWAY_BASE_URL }),
92+
})
8593

8694
const result = streamText({
8795
model: openai('gpt-4o'),

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55
* and progress streaming via Server-Sent Events.
66
*/
77

8-
import { HackMDClient } from '@/lib/hackmd-client'
8+
import { NotePermissionRole } from '@hackmd/api'
9+
import { createHackMDApi } from '@/lib/create-hackmd-api'
10+
11+
function isRateLimitError(err: unknown): boolean {
12+
if (typeof err === 'object' && err !== null) {
13+
const e = err as { code?: number; response?: { status?: number } }
14+
if (e.code === 429 || e.response?.status === 429) return true
15+
}
16+
const msg = err instanceof Error ? err.message : String(err)
17+
return msg.includes('429') || msg.toLowerCase().includes('too many requests')
18+
}
919

1020
export const maxDuration = 300
1121

@@ -40,10 +50,7 @@ export async function POST(req: Request) {
4050
})
4151
}
4252

43-
const client = new HackMDClient({
44-
accessToken: config.apiKey,
45-
apiEndpoint: config.apiEndpoint,
46-
})
53+
const client = createHackMDApi(config.apiKey, config.apiEndpoint)
4754

4855
const webDomain = config.webDomain || 'https://hackmd.io'
4956
const delayMs = Math.max(0, Math.min(config.delayMs || 300, 5000))
@@ -81,8 +88,8 @@ export async function POST(req: Request) {
8188
const note = await client.createTeamNote(config.teamPath, {
8289
title: page.title,
8390
content: page.content,
84-
readPermission: 'guest',
85-
writePermission: 'signed_in',
91+
readPermission: NotePermissionRole.GUEST,
92+
writePermission: NotePermissionRole.SIGNED_IN,
8693
})
8794

8895
createdNotes[page.sessionId] = note.shortId
@@ -119,7 +126,7 @@ export async function POST(req: Request) {
119126
})
120127

121128
// If it's a rate limit error, wait longer
122-
if (message.includes('429')) {
129+
if (isRateLimitError(err)) {
123130
await new Promise(resolve => setTimeout(resolve, 10000))
124131
} else if (delayMs > 0) {
125132
await new Promise(resolve => setTimeout(resolve, delayMs))
@@ -150,8 +157,8 @@ export async function POST(req: Request) {
150157
const mainNote = await client.createTeamNote(config.teamPath, {
151158
title: homepage.title,
152159
content: homepageContent,
153-
readPermission: 'guest',
154-
writePermission: 'signed_in',
160+
readPermission: NotePermissionRole.GUEST,
161+
writePermission: NotePermissionRole.SIGNED_IN,
155162
})
156163

157164
completed++
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Verifies HackMD credentials server-side. The HackMD API does not allow
3+
* browser origins (CORS), so /me must be called from the backend.
4+
*/
5+
6+
import { createHackMDApi } from '@/lib/create-hackmd-api'
7+
8+
function httpStatusFromError(err: unknown): number | undefined {
9+
if (typeof err !== 'object' || err === null) return undefined
10+
const e = err as { response?: { status?: number }; code?: number }
11+
if (e.response?.status != null) return e.response.status
12+
if (typeof e.code === 'number') return e.code
13+
return undefined
14+
}
15+
16+
export async function POST(req: Request) {
17+
const body = await req.json()
18+
const { apiKey, apiEndpoint, teamPath } = body as {
19+
apiKey?: string
20+
apiEndpoint?: string
21+
teamPath?: string
22+
}
23+
24+
if (!apiKey?.trim()) {
25+
return Response.json({ error: 'HackMD API key is required' }, { status: 400 })
26+
}
27+
28+
const client = createHackMDApi(apiKey, apiEndpoint)
29+
30+
let user: { teams?: Array<{ path: string }> }
31+
try {
32+
user = await client.getMe()
33+
} catch (err) {
34+
const status = httpStatusFromError(err)
35+
if (status === undefined) {
36+
return Response.json(
37+
{ error: 'Failed to reach HackMD API. Check the API endpoint URL.' },
38+
{ status: 502 },
39+
)
40+
}
41+
return Response.json(
42+
{ error: `HackMD API returned ${status}` },
43+
{ status: status === 401 ? 401 : 502 },
44+
)
45+
}
46+
47+
const teams = user.teams || []
48+
const trimmedTeam = teamPath?.trim()
49+
50+
if (trimmedTeam && !teams.some((t) => t.path === trimmedTeam)) {
51+
const teamNames = teams.map((t) => t.path).join(', ')
52+
return Response.json(
53+
{
54+
error: `Team "${trimmedTeam}" not found. Available teams: ${teamNames || 'none'}`,
55+
},
56+
{ status: 400 },
57+
)
58+
}
59+
60+
return Response.json({
61+
teamPath: trimmedTeam || teams[0]?.path || '',
62+
})
63+
}

0 commit comments

Comments
 (0)