lukasgross commited on
Commit
69f2236
·
1 Parent(s): d3763f3

Add unmanged sample with a minimal backend (#98)

Browse files

* Move existing starter app to managed-chatkit

* Add chatkit example

* Update CI

* Small fixes

* Remove to_message_content

* Simplify

* Next -> Vite

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/ci.yml +59 -2
  2. README.md +5 -68
  3. app/App.tsx +0 -34
  4. app/api/create-session/route.ts +0 -283
  5. app/layout.tsx +0 -26
  6. app/page.tsx +0 -5
  7. chatkit/.env.example +3 -0
  8. chatkit/.gitignore +3 -0
  9. chatkit/README.md +32 -0
  10. chatkit/backend/.gitignore +9 -0
  11. chatkit/backend/app/__init__.py +1 -0
  12. chatkit/backend/app/assistant.py +31 -0
  13. chatkit/backend/app/main.py +35 -0
  14. chatkit/backend/app/memory_store.py +196 -0
  15. chatkit/backend/app/server.py +54 -0
  16. chatkit/backend/pyproject.toml +21 -0
  17. chatkit/backend/scripts/run.sh +40 -0
  18. chatkit/frontend/.gitignore +7 -0
  19. chatkit/frontend/eslint.config.mjs +37 -0
  20. chatkit/frontend/index.html +15 -0
  21. chatkit/frontend/package-lock.json +0 -0
  22. chatkit/frontend/package.json +39 -0
  23. postcss.config.mjs → chatkit/frontend/postcss.config.mjs +3 -1
  24. {app → chatkit/frontend/public}/favicon.ico +0 -0
  25. chatkit/frontend/src/App.tsx +11 -0
  26. chatkit/frontend/src/components/ChatKitPanel.tsx +18 -0
  27. app/globals.css → chatkit/frontend/src/index.css +0 -0
  28. chatkit/frontend/src/lib/config.ts +16 -0
  29. chatkit/frontend/src/main.tsx +16 -0
  30. chatkit/frontend/src/vite-env.d.ts +14 -0
  31. chatkit/frontend/tsconfig.json +17 -0
  32. chatkit/frontend/tsconfig.node.json +11 -0
  33. chatkit/frontend/vite.config.ts +21 -0
  34. chatkit/package-lock.json +327 -0
  35. chatkit/package.json +12 -0
  36. components/ChatKitPanel.tsx +0 -418
  37. components/ErrorOverlay.tsx +0 -44
  38. eslint.config.mjs +0 -25
  39. hooks/useColorScheme.ts +0 -178
  40. lib/config.ts +0 -35
  41. .env.example → managed-chatkit/.env.example +0 -0
  42. .gitignore → managed-chatkit/.gitignore +11 -12
  43. managed-chatkit/README.md +35 -0
  44. managed-chatkit/backend/.gitignore +9 -0
  45. managed-chatkit/backend/app/__init__.py +1 -0
  46. managed-chatkit/backend/app/main.py +166 -0
  47. managed-chatkit/backend/pyproject.toml +20 -0
  48. managed-chatkit/backend/scripts/run.sh +42 -0
  49. managed-chatkit/frontend/eslint.config.mjs +56 -0
  50. managed-chatkit/frontend/index.html +15 -0
.github/workflows/ci.yml CHANGED
@@ -6,9 +6,62 @@ on:
6
  pull_request:
7
 
8
  jobs:
9
- build:
10
- name: Install, Lint, and Build
11
  runs-on: ubuntu-latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  steps:
14
  - name: Checkout repository
@@ -18,14 +71,18 @@ jobs:
18
  uses: actions/setup-node@v4
19
  with:
20
  node-version: 22
 
21
  cache: "npm"
22
 
23
  - name: Install dependencies
 
24
  run: npm ci
25
 
26
  - name: Run ESLint
 
27
  run: npm run lint
28
 
29
  - name: Run build
 
30
  run: npm run build
31
 
 
6
  pull_request:
7
 
8
  jobs:
9
+ python:
10
+ name: Python checks (${{ matrix.project }})
11
  runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ project:
16
+ - chatkit/backend
17
+ - managed-chatkit/backend
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.13"
26
+
27
+ - name: Install dependencies (${{ matrix.project }})
28
+ working-directory: ${{ matrix.project }}
29
+ env:
30
+ PIP_DISABLE_PIP_VERSION_CHECK: "1"
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ python -m pip install ".[dev]"
34
+ python -m pip install mypy
35
+
36
+ - name: Syntax check (${{ matrix.project }})
37
+ working-directory: ${{ matrix.project }}
38
+ run: python -m compileall app
39
+
40
+ - name: Ruff lint (${{ matrix.project }})
41
+ working-directory: ${{ matrix.project }}
42
+ run: python -m ruff check app
43
+
44
+ - name: Ruff format check (${{ matrix.project }})
45
+ working-directory: ${{ matrix.project }}
46
+ run: |
47
+ python -m ruff format --check app || {
48
+ echo "::error ::Ruff format check failed. Run 'python -m ruff format app' locally to apply formatting.";
49
+ exit 1;
50
+ }
51
+
52
+ - name: mypy (${{ matrix.project }})
53
+ working-directory: ${{ matrix.project }}
54
+ run: python -m mypy app --ignore-missing-imports
55
+
56
+ node:
57
+ name: Node checks (${{ matrix.project }})
58
+ runs-on: ubuntu-latest
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ project:
63
+ - managed-chatkit/frontend
64
+ - chatkit/frontend
65
 
66
  steps:
67
  - name: Checkout repository
 
71
  uses: actions/setup-node@v4
72
  with:
73
  node-version: 22
74
+ cache-dependency-path: ${{ matrix.project }}/package-lock.json
75
  cache: "npm"
76
 
77
  - name: Install dependencies
78
+ working-directory: ${{ matrix.project }}
79
  run: npm ci
80
 
81
  - name: Run ESLint
82
+ working-directory: ${{ matrix.project }}
83
  run: npm run lint
84
 
85
  - name: Run build
86
+ working-directory: ${{ matrix.project }}
87
  run: npm run build
88
 
README.md CHANGED
@@ -1,71 +1,8 @@
1
- # ChatKit Starter Template
2
 
3
- [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
4
- ![NextJS](https://img.shields.io/badge/Built_with-NextJS-blue)
5
- ![OpenAI API](https://img.shields.io/badge/Powered_by-OpenAI_API-orange)
6
 
7
- This repository is the simplest way to bootstrap a [ChatKit](http://openai.github.io/chatkit-js/) application. It ships with a minimal Next.js UI, the ChatKit web component, and a ready-to-use session endpoint so you can experiment with OpenAI-hosted workflows built using [Agent Builder](https://platform.openai.com/agent-builder).
8
 
9
- ## What You Get
10
-
11
- - Next.js app with `<openai-chatkit>` web component and theming controls
12
- - API endpoint for creating a session at [`app/api/create-session/route.ts`](app/api/create-session/route.ts)
13
- - Config file for starter prompts, theme, placeholder text, and greeting message
14
-
15
- ## Getting Started
16
-
17
- ### 1. Install dependencies
18
-
19
- ```bash
20
- npm install
21
- ```
22
-
23
- ### 2. Create your environment file
24
-
25
- Copy the example file and fill in the required values:
26
-
27
- ```bash
28
- cp .env.example .env.local
29
- ```
30
-
31
- You can get your workflow id from the [Agent Builder](https://platform.openai.com/agent-builder) interface, after clicking "Publish":
32
-
33
- <img src="./public/docs/workflow.jpg" width=500 />
34
-
35
- You can get your OpenAI API key from the [OpenAI API Keys](https://platform.openai.com/api-keys) page.
36
-
37
- ### 3. Configure ChatKit credentials
38
-
39
- Update `.env.local` with the variables that match your setup.
40
-
41
- - `OPENAI_API_KEY` — This must be an API key created **within the same org & project as your Agent Builder**. If you already have a different `OPENAI_API_KEY` env variable set in your terminal session, that one will take precedence over the key in `.env.local` one (this is how a Next.js app works). So, **please run `unset OPENAI_API_KEY` (`set OPENAI_API_KEY=` for Windows OS) beforehand**.
42
- - `NEXT_PUBLIC_CHATKIT_WORKFLOW_ID` — This is the ID of the workflow you created in [Agent Builder](https://platform.openai.com/agent-builder), which starts with `wf_...`
43
- - (optional) `CHATKIT_API_BASE` - This is a customizable base URL for the ChatKit API endpoint
44
-
45
- > Note: if your workflow is using a model requiring organization verification, such as GPT-5, make sure you verify your organization first. Visit your [organization settings](https://platform.openai.com/settings/organization/general) and click on "Verify Organization".
46
-
47
- ### 4. Run the app
48
-
49
- ```bash
50
- npm run dev
51
- ```
52
-
53
- Visit `http://localhost:3000` and start chatting. Use the prompts on the start screen to verify your workflow connection, then customize the UI or prompt list in [`lib/config.ts`](lib/config.ts) and [`components/ChatKitPanel.tsx`](components/ChatKitPanel.tsx).
54
-
55
- ### 5. Deploy your app
56
-
57
- ```bash
58
- npm run build
59
- ```
60
-
61
- Before deploying your app, you need to verify the domain by adding it to the [Domain allowlist](https://platform.openai.com/settings/organization/security/domain-allowlist) on your dashboard.
62
-
63
- ## Customization Tips
64
-
65
- - Adjust starter prompts, greeting text, [chatkit theme](https://chatkit.studio/playground), and placeholder copy in [`lib/config.ts`](lib/config.ts).
66
- - Update the event handlers inside [`components/.tsx`](components/ChatKitPanel.tsx) to integrate with your product analytics or storage.
67
-
68
- ## References
69
-
70
- - [ChatKit JavaScript Library](http://openai.github.io/chatkit-js/)
71
- - [Advanced Self-Hosting Examples](https://github.com/openai/openai-chatkit-advanced-samples)
 
1
+ # OpenAI ChatKit Starter Templates
2
 
3
+ This repository contains two starter apps as reference implementations of minimal ChatKit integrations.
 
 
4
 
5
+ You can run the following examples:
6
 
7
+ - [**ChatKit**](chatkit) - example of a self-hosted ChatKit integration.
8
+ - [**Managed ChatKit**](managed-chatkit) – example of a managed ChatKit integration with hosted workflows.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/App.tsx DELETED
@@ -1,34 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback } from "react";
4
- import { ChatKitPanel, type FactAction } from "@/components/ChatKitPanel";
5
- import { useColorScheme } from "@/hooks/useColorScheme";
6
-
7
- export default function App() {
8
- const { scheme, setScheme } = useColorScheme();
9
-
10
- const handleWidgetAction = useCallback(async (action: FactAction) => {
11
- if (process.env.NODE_ENV !== "production") {
12
- console.info("[ChatKitPanel] widget action", action);
13
- }
14
- }, []);
15
-
16
- const handleResponseEnd = useCallback(() => {
17
- if (process.env.NODE_ENV !== "production") {
18
- console.debug("[ChatKitPanel] response end");
19
- }
20
- }, []);
21
-
22
- return (
23
- <main className="flex min-h-screen flex-col items-center justify-end bg-slate-100 dark:bg-slate-950">
24
- <div className="mx-auto w-full max-w-5xl">
25
- <ChatKitPanel
26
- theme={scheme}
27
- onWidgetAction={handleWidgetAction}
28
- onResponseEnd={handleResponseEnd}
29
- onThemeRequest={setScheme}
30
- />
31
- </div>
32
- </main>
33
- );
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/create-session/route.ts DELETED
@@ -1,283 +0,0 @@
1
- import { WORKFLOW_ID } from "@/lib/config";
2
-
3
- export const runtime = "edge";
4
-
5
- interface CreateSessionRequestBody {
6
- workflow?: { id?: string | null } | null;
7
- scope?: { user_id?: string | null } | null;
8
- workflowId?: string | null;
9
- chatkit_configuration?: {
10
- file_upload?: {
11
- enabled?: boolean;
12
- };
13
- };
14
- }
15
-
16
- const DEFAULT_CHATKIT_BASE = "https://api.openai.com";
17
- const SESSION_COOKIE_NAME = "chatkit_session_id";
18
- const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
19
-
20
- export async function POST(request: Request): Promise<Response> {
21
- if (request.method !== "POST") {
22
- return methodNotAllowedResponse();
23
- }
24
- let sessionCookie: string | null = null;
25
- try {
26
- const openaiApiKey = process.env.OPENAI_API_KEY;
27
- if (!openaiApiKey) {
28
- return new Response(
29
- JSON.stringify({
30
- error: "Missing OPENAI_API_KEY environment variable",
31
- }),
32
- {
33
- status: 500,
34
- headers: { "Content-Type": "application/json" },
35
- }
36
- );
37
- }
38
-
39
- const parsedBody = await safeParseJson<CreateSessionRequestBody>(request);
40
- const { userId, sessionCookie: resolvedSessionCookie } =
41
- await resolveUserId(request);
42
- sessionCookie = resolvedSessionCookie;
43
- const resolvedWorkflowId =
44
- parsedBody?.workflow?.id ?? parsedBody?.workflowId ?? WORKFLOW_ID;
45
-
46
- if (process.env.NODE_ENV !== "production") {
47
- console.info("[create-session] handling request", {
48
- resolvedWorkflowId,
49
- body: JSON.stringify(parsedBody),
50
- });
51
- }
52
-
53
- if (!resolvedWorkflowId) {
54
- return buildJsonResponse(
55
- { error: "Missing workflow id" },
56
- 400,
57
- { "Content-Type": "application/json" },
58
- sessionCookie
59
- );
60
- }
61
-
62
- const apiBase = process.env.CHATKIT_API_BASE ?? DEFAULT_CHATKIT_BASE;
63
- const url = `${apiBase}/v1/chatkit/sessions`;
64
- const upstreamResponse = await fetch(url, {
65
- method: "POST",
66
- headers: {
67
- "Content-Type": "application/json",
68
- Authorization: `Bearer ${openaiApiKey}`,
69
- "OpenAI-Beta": "chatkit_beta=v1",
70
- },
71
- body: JSON.stringify({
72
- workflow: { id: resolvedWorkflowId },
73
- user: userId,
74
- chatkit_configuration: {
75
- file_upload: {
76
- enabled:
77
- parsedBody?.chatkit_configuration?.file_upload?.enabled ?? false,
78
- },
79
- },
80
- }),
81
- });
82
-
83
- if (process.env.NODE_ENV !== "production") {
84
- console.info("[create-session] upstream response", {
85
- status: upstreamResponse.status,
86
- statusText: upstreamResponse.statusText,
87
- });
88
- }
89
-
90
- const upstreamJson = (await upstreamResponse.json().catch(() => ({}))) as
91
- | Record<string, unknown>
92
- | undefined;
93
-
94
- if (!upstreamResponse.ok) {
95
- const upstreamError = extractUpstreamError(upstreamJson);
96
- console.error("OpenAI ChatKit session creation failed", {
97
- status: upstreamResponse.status,
98
- statusText: upstreamResponse.statusText,
99
- body: upstreamJson,
100
- });
101
- return buildJsonResponse(
102
- {
103
- error:
104
- upstreamError ??
105
- `Failed to create session: ${upstreamResponse.statusText}`,
106
- details: upstreamJson,
107
- },
108
- upstreamResponse.status,
109
- { "Content-Type": "application/json" },
110
- sessionCookie
111
- );
112
- }
113
-
114
- const clientSecret = upstreamJson?.client_secret ?? null;
115
- const expiresAfter = upstreamJson?.expires_after ?? null;
116
- const responsePayload = {
117
- client_secret: clientSecret,
118
- expires_after: expiresAfter,
119
- };
120
-
121
- return buildJsonResponse(
122
- responsePayload,
123
- 200,
124
- { "Content-Type": "application/json" },
125
- sessionCookie
126
- );
127
- } catch (error) {
128
- console.error("Create session error", error);
129
- return buildJsonResponse(
130
- { error: "Unexpected error" },
131
- 500,
132
- { "Content-Type": "application/json" },
133
- sessionCookie
134
- );
135
- }
136
- }
137
-
138
- export async function GET(): Promise<Response> {
139
- return methodNotAllowedResponse();
140
- }
141
-
142
- function methodNotAllowedResponse(): Response {
143
- return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
144
- status: 405,
145
- headers: { "Content-Type": "application/json" },
146
- });
147
- }
148
-
149
- async function resolveUserId(request: Request): Promise<{
150
- userId: string;
151
- sessionCookie: string | null;
152
- }> {
153
- const existing = getCookieValue(
154
- request.headers.get("cookie"),
155
- SESSION_COOKIE_NAME
156
- );
157
- if (existing) {
158
- return { userId: existing, sessionCookie: null };
159
- }
160
-
161
- const generated =
162
- typeof crypto.randomUUID === "function"
163
- ? crypto.randomUUID()
164
- : Math.random().toString(36).slice(2);
165
-
166
- return {
167
- userId: generated,
168
- sessionCookie: serializeSessionCookie(generated),
169
- };
170
- }
171
-
172
- function getCookieValue(
173
- cookieHeader: string | null,
174
- name: string
175
- ): string | null {
176
- if (!cookieHeader) {
177
- return null;
178
- }
179
-
180
- const cookies = cookieHeader.split(";");
181
- for (const cookie of cookies) {
182
- const [rawName, ...rest] = cookie.split("=");
183
- if (!rawName || rest.length === 0) {
184
- continue;
185
- }
186
- if (rawName.trim() === name) {
187
- return rest.join("=").trim();
188
- }
189
- }
190
- return null;
191
- }
192
-
193
- function serializeSessionCookie(value: string): string {
194
- const attributes = [
195
- `${SESSION_COOKIE_NAME}=${encodeURIComponent(value)}`,
196
- "Path=/",
197
- `Max-Age=${SESSION_COOKIE_MAX_AGE}`,
198
- "HttpOnly",
199
- "SameSite=Lax",
200
- ];
201
-
202
- if (process.env.NODE_ENV === "production") {
203
- attributes.push("Secure");
204
- }
205
- return attributes.join("; ");
206
- }
207
-
208
- function buildJsonResponse(
209
- payload: unknown,
210
- status: number,
211
- headers: Record<string, string>,
212
- sessionCookie: string | null
213
- ): Response {
214
- const responseHeaders = new Headers(headers);
215
-
216
- if (sessionCookie) {
217
- responseHeaders.append("Set-Cookie", sessionCookie);
218
- }
219
-
220
- return new Response(JSON.stringify(payload), {
221
- status,
222
- headers: responseHeaders,
223
- });
224
- }
225
-
226
- async function safeParseJson<T>(req: Request): Promise<T | null> {
227
- try {
228
- const text = await req.text();
229
- if (!text) {
230
- return null;
231
- }
232
- return JSON.parse(text) as T;
233
- } catch {
234
- return null;
235
- }
236
- }
237
-
238
- function extractUpstreamError(
239
- payload: Record<string, unknown> | undefined
240
- ): string | null {
241
- if (!payload) {
242
- return null;
243
- }
244
-
245
- const error = payload.error;
246
- if (typeof error === "string") {
247
- return error;
248
- }
249
-
250
- if (
251
- error &&
252
- typeof error === "object" &&
253
- "message" in error &&
254
- typeof (error as { message?: unknown }).message === "string"
255
- ) {
256
- return (error as { message: string }).message;
257
- }
258
-
259
- const details = payload.details;
260
- if (typeof details === "string") {
261
- return details;
262
- }
263
-
264
- if (details && typeof details === "object" && "error" in details) {
265
- const nestedError = (details as { error?: unknown }).error;
266
- if (typeof nestedError === "string") {
267
- return nestedError;
268
- }
269
- if (
270
- nestedError &&
271
- typeof nestedError === "object" &&
272
- "message" in nestedError &&
273
- typeof (nestedError as { message?: unknown }).message === "string"
274
- ) {
275
- return (nestedError as { message: string }).message;
276
- }
277
- }
278
-
279
- if (typeof payload.message === "string") {
280
- return payload.message;
281
- }
282
- return null;
283
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/layout.tsx DELETED
@@ -1,26 +0,0 @@
1
- import Script from "next/script";
2
- import type { Metadata } from "next";
3
- import "./globals.css";
4
-
5
- export const metadata: Metadata = {
6
- title: "AgentKit demo",
7
- description: "Demo of ChatKit with hosted workflow",
8
- };
9
-
10
- export default function RootLayout({
11
- children,
12
- }: Readonly<{
13
- children: React.ReactNode;
14
- }>) {
15
- return (
16
- <html lang="en">
17
- <head>
18
- <Script
19
- src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
20
- strategy="beforeInteractive"
21
- />
22
- </head>
23
- <body className="antialiased">{children}</body>
24
- </html>
25
- );
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/page.tsx DELETED
@@ -1,5 +0,0 @@
1
- import App from "./App";
2
-
3
- export default function Home() {
4
- return <App />;
5
- }
 
 
 
 
 
 
chatkit/.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ OPENAI_API_KEY=sk-proj-...
2
+ VITE_CHATKIT_API_URL=http://127.0.0.1:8000/chatkit
3
+ VITE_CHATKIT_API_DOMAIN_KEY=domain_pk_local_dev
chatkit/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules/
2
+ .env*
3
+ !.env.example
chatkit/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChatKit Starter
2
+
3
+ Minimal Vite + React UI paired with a FastAPI backend that forwards chat
4
+ requests to OpenAI through the ChatKit server library.
5
+
6
+ ## Quick start
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ What happens:
14
+
15
+ - `npm run dev` starts the FastAPI backend on `127.0.0.1:8000` and the Vite
16
+ frontend on `127.0.0.1:3000` with a proxy at `/chatkit`.
17
+
18
+ ## Required environment
19
+
20
+ - `OPENAI_API_KEY` (backend)
21
+ - `VITE_CHATKIT_API_URL` (optional, defaults to `/chatkit`)
22
+ - `VITE_CHATKIT_API_DOMAIN_KEY` (optional, defaults to `domain_pk_localhost_dev`)
23
+
24
+ Set `OPENAI_API_KEY` in your shell or in `.env.local` at the repo root before
25
+ running the backend. Register a production domain key in the OpenAI dashboard
26
+ and set `VITE_CHATKIT_API_DOMAIN_KEY` when deploying.
27
+
28
+ ## Customize
29
+
30
+ - Update UI and connection settings in `frontend/src/lib/config.ts`.
31
+ - Adjust layout in `frontend/src/components/ChatKitPanel.tsx`.
32
+ - Swap the in-memory store in `backend/app/server.py` for persistence.
chatkit/backend/.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .env
6
+ .ruff_cache/
7
+ .pytest_cache/
8
+ .coverage/
9
+ *.log
chatkit/backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Minimal ChatKit backend package."""
chatkit/backend/app/assistant.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple streaming assistant wired to ChatKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Annotated
6
+
7
+ from agents import Agent
8
+ from chatkit.agents import AgentContext
9
+ from pydantic import ConfigDict, Field
10
+ from .memory_store import MemoryStore
11
+
12
+ MODEL = "gpt-4.1-mini"
13
+
14
+
15
+ class StarterAgentContext(AgentContext):
16
+ """Minimal context passed into the ChatKit agent runner."""
17
+
18
+ model_config = ConfigDict(arbitrary_types_allowed=True)
19
+ request_context: dict[str, Any]
20
+ # The store is excluded so it isn't serialized into prompts.
21
+ store: Annotated[MemoryStore, Field(exclude=True)]
22
+
23
+
24
+ assistant_agent = Agent[StarterAgentContext](
25
+ model=MODEL,
26
+ name="Starter Assistant",
27
+ instructions=(
28
+ "You are a concise, helpful assistant. "
29
+ "Keep replies short and focus on directly answering the user's request."
30
+ ),
31
+ )
chatkit/backend/app/main.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI entrypoint for the ChatKit starter backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from chatkit.server import StreamingResult
6
+ from fastapi import FastAPI, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
9
+
10
+ from .server import StarterChatServer
11
+
12
+ app = FastAPI(title="ChatKit Starter API")
13
+
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ chatkit_server = StarterChatServer()
23
+
24
+
25
+ @app.post("/chatkit")
26
+ async def chatkit_endpoint(request: Request) -> Response:
27
+ """Proxy the ChatKit web component payload to the server implementation."""
28
+ payload = await request.body()
29
+ result = await chatkit_server.process(payload, {"request": request})
30
+
31
+ if isinstance(result, StreamingResult):
32
+ return StreamingResponse(result, media_type="text/event-stream")
33
+ if hasattr(result, "json"):
34
+ return Response(content=result.json, media_type="application/json")
35
+ return JSONResponse(result)
chatkit/backend/app/memory_store.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple in-memory store compatible with the ChatKit Store interface.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List
10
+
11
+ from chatkit.store import NotFoundError, Store
12
+ from chatkit.types import Attachment, Page, Thread, ThreadItem, ThreadMetadata
13
+
14
+
15
+ @dataclass
16
+ class _ThreadState:
17
+ thread: ThreadMetadata
18
+ items: List[ThreadItem]
19
+
20
+
21
+ class MemoryStore(Store[dict[str, Any]]):
22
+ """Simple in-memory store compatible with the ChatKit Store interface."""
23
+
24
+ def __init__(self) -> None:
25
+ self._threads: Dict[str, _ThreadState] = {}
26
+ # Attachments intentionally unsupported; use a real store that enforces auth.
27
+
28
+ @staticmethod
29
+ def _coerce_thread_metadata(thread: ThreadMetadata | Thread) -> ThreadMetadata:
30
+ """Return thread metadata without any embedded items."""
31
+ has_items = isinstance(thread, Thread) or "items" in getattr(
32
+ thread, "model_fields_set", set()
33
+ )
34
+ if not has_items:
35
+ return thread.model_copy(deep=True)
36
+
37
+ data = thread.model_dump()
38
+ data.pop("items", None)
39
+ return ThreadMetadata(**data).model_copy(deep=True)
40
+
41
+ # -- Thread metadata -------------------------------------------------
42
+ async def load_thread(
43
+ self, thread_id: str, context: dict[str, Any]
44
+ ) -> ThreadMetadata:
45
+ state = self._threads.get(thread_id)
46
+ if not state:
47
+ raise NotFoundError(f"Thread {thread_id} not found")
48
+ return self._coerce_thread_metadata(state.thread)
49
+
50
+ async def save_thread(
51
+ self, thread: ThreadMetadata, context: dict[str, Any]
52
+ ) -> None:
53
+ metadata = self._coerce_thread_metadata(thread)
54
+ state = self._threads.get(thread.id)
55
+ if state:
56
+ state.thread = metadata
57
+ else:
58
+ self._threads[thread.id] = _ThreadState(
59
+ thread=metadata,
60
+ items=[],
61
+ )
62
+
63
+ async def load_threads(
64
+ self,
65
+ limit: int,
66
+ after: str | None,
67
+ order: str,
68
+ context: dict[str, Any],
69
+ ) -> Page[ThreadMetadata]:
70
+ threads = sorted(
71
+ (
72
+ self._coerce_thread_metadata(state.thread)
73
+ for state in self._threads.values()
74
+ ),
75
+ key=lambda t: t.created_at or datetime.min,
76
+ reverse=(order == "desc"),
77
+ )
78
+
79
+ if after:
80
+ index_map = {thread.id: idx for idx, thread in enumerate(threads)}
81
+ start = index_map.get(after, -1) + 1
82
+ else:
83
+ start = 0
84
+
85
+ slice_threads = threads[start : start + limit + 1]
86
+ has_more = len(slice_threads) > limit
87
+ slice_threads = slice_threads[:limit]
88
+ next_after = slice_threads[-1].id if has_more and slice_threads else None
89
+ return Page(
90
+ data=slice_threads,
91
+ has_more=has_more,
92
+ after=next_after,
93
+ )
94
+
95
+ async def delete_thread(self, thread_id: str, context: dict[str, Any]) -> None:
96
+ self._threads.pop(thread_id, None)
97
+
98
+ # -- Thread items ----------------------------------------------------
99
+ def _thread_state(self, thread_id: str) -> _ThreadState:
100
+ state = self._threads.get(thread_id)
101
+ if state is None:
102
+ state = _ThreadState(
103
+ thread=ThreadMetadata(id=thread_id, created_at=datetime.utcnow()),
104
+ items=[],
105
+ )
106
+ self._threads[thread_id] = state
107
+ return state
108
+
109
+ def _items(self, thread_id: str) -> List[ThreadItem]:
110
+ state = self._thread_state(thread_id)
111
+ return state.items
112
+
113
+ async def load_thread_items(
114
+ self,
115
+ thread_id: str,
116
+ after: str | None,
117
+ limit: int,
118
+ order: str,
119
+ context: dict[str, Any],
120
+ ) -> Page[ThreadItem]:
121
+ items = [item.model_copy(deep=True) for item in self._items(thread_id)]
122
+ items.sort(
123
+ key=lambda item: getattr(item, "created_at", datetime.utcnow()),
124
+ reverse=(order == "desc"),
125
+ )
126
+
127
+ if after:
128
+ index_map = {item.id: idx for idx, item in enumerate(items)}
129
+ start = index_map.get(after, -1) + 1
130
+ else:
131
+ start = 0
132
+
133
+ slice_items = items[start : start + limit + 1]
134
+ has_more = len(slice_items) > limit
135
+ slice_items = slice_items[:limit]
136
+ next_after = slice_items[-1].id if has_more and slice_items else None
137
+ return Page(data=slice_items, has_more=has_more, after=next_after)
138
+
139
+ async def add_thread_item(
140
+ self, thread_id: str, item: ThreadItem, context: dict[str, Any]
141
+ ) -> None:
142
+ self._items(thread_id).append(item.model_copy(deep=True))
143
+
144
+ async def save_item(
145
+ self, thread_id: str, item: ThreadItem, context: dict[str, Any]
146
+ ) -> None:
147
+ items = self._items(thread_id)
148
+ for idx, existing in enumerate(items):
149
+ if existing.id == item.id:
150
+ items[idx] = item.model_copy(deep=True)
151
+ return
152
+ items.append(item.model_copy(deep=True))
153
+
154
+ async def load_item(
155
+ self, thread_id: str, item_id: str, context: dict[str, Any]
156
+ ) -> ThreadItem:
157
+ for item in self._items(thread_id):
158
+ if item.id == item_id:
159
+ return item.model_copy(deep=True)
160
+ raise NotFoundError(f"Item {item_id} not found")
161
+
162
+ async def delete_thread_item(
163
+ self, thread_id: str, item_id: str, context: dict[str, Any]
164
+ ) -> None:
165
+ items = self._items(thread_id)
166
+ self._threads[thread_id].items = [item for item in items if item.id != item_id]
167
+
168
+ # -- Files -----------------------------------------------------------
169
+ # These methods are not currently used but required to be compatible with the Store interface.
170
+
171
+ async def save_attachment(
172
+ self,
173
+ attachment: Attachment,
174
+ context: dict[str, Any],
175
+ ) -> None:
176
+ raise NotImplementedError(
177
+ "MemoryStore does not persist attachments. Provide a Store implementation "
178
+ "that enforces authentication and authorization before enabling uploads."
179
+ )
180
+
181
+ async def load_attachment(
182
+ self,
183
+ attachment_id: str,
184
+ context: dict[str, Any],
185
+ ) -> Attachment:
186
+ raise NotImplementedError(
187
+ "MemoryStore does not load attachments. Provide a Store implementation "
188
+ "that enforces authentication and authorization before enabling uploads."
189
+ )
190
+
191
+ async def delete_attachment(
192
+ self, attachment_id: str, context: dict[str, Any]
193
+ ) -> None:
194
+ raise NotImplementedError(
195
+ "MemoryStore does not delete attachments because they are never stored."
196
+ )
chatkit/backend/app/server.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ChatKit server that streams responses from a single assistant."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, AsyncIterator
6
+
7
+ from agents import Runner
8
+ from chatkit.agents import simple_to_agent_input, stream_agent_response
9
+ from chatkit.server import ChatKitServer
10
+ from chatkit.types import ThreadMetadata, ThreadStreamEvent, UserMessageItem
11
+
12
+ from .assistant import StarterAgentContext, assistant_agent
13
+ from .memory_store import MemoryStore
14
+
15
+ MAX_RECENT_ITEMS = 30
16
+
17
+
18
+ class StarterChatServer(ChatKitServer[dict[str, Any]]):
19
+ """Server implementation that keeps conversation state in memory."""
20
+
21
+ def __init__(self) -> None:
22
+ self.store: MemoryStore = MemoryStore()
23
+ super().__init__(self.store)
24
+
25
+ async def respond(
26
+ self,
27
+ thread: ThreadMetadata,
28
+ item: UserMessageItem | None,
29
+ context: dict[str, Any],
30
+ ) -> AsyncIterator[ThreadStreamEvent]:
31
+ items_page = await self.store.load_thread_items(
32
+ thread.id,
33
+ after=None,
34
+ limit=MAX_RECENT_ITEMS,
35
+ order="desc",
36
+ context=context,
37
+ )
38
+ items = list(reversed(items_page.data))
39
+ agent_input = await simple_to_agent_input(items)
40
+
41
+ agent_context = StarterAgentContext(
42
+ thread=thread,
43
+ store=self.store,
44
+ request_context=context,
45
+ )
46
+
47
+ result = Runner.run_streamed(
48
+ assistant_agent,
49
+ agent_input,
50
+ context=agent_context,
51
+ )
52
+
53
+ async for event in stream_agent_response(agent_context, result):
54
+ yield event
chatkit/backend/pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "chatkit-starter-backend"
3
+ version = "0.1.0"
4
+ description = "Minimal FastAPI backend for the self-hosted ChatKit starter"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "fastapi>=0.114,<0.116",
8
+ "uvicorn[standard]>=0.36,<0.37",
9
+ "openai>=1.40",
10
+ "openai-chatkit>=1.4.0,<2",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = [
15
+ "ruff>=0.6.4,<0.7",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=68.0", "wheel"]
20
+ build-backend = "setuptools.build_meta"
21
+
chatkit/backend/scripts/run.sh ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Simple helper to start the ChatKit backend (similar to cat-lounge UX).
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
9
+
10
+ cd "$PROJECT_ROOT"
11
+
12
+ if [ ! -d ".venv" ]; then
13
+ echo "Creating virtual env in $PROJECT_ROOT/.venv ..."
14
+ python -m venv .venv
15
+ fi
16
+
17
+ source .venv/bin/activate
18
+
19
+ echo "Installing backend deps (editable) ..."
20
+ pip install -e . >/dev/null
21
+
22
+ # Load env vars from the repo's .env.local (if present) so OPENAI_API_KEY
23
+ # does not need to be exported manually.
24
+ ENV_FILE="$PROJECT_ROOT/../.env.local"
25
+ if [ -z "${OPENAI_API_KEY:-}" ] && [ -f "$ENV_FILE" ]; then
26
+ echo "Sourcing OPENAI_API_KEY from $ENV_FILE"
27
+ # shellcheck disable=SC1090
28
+ set -a
29
+ . "$ENV_FILE"
30
+ set +a
31
+ fi
32
+
33
+ if [ -z "${OPENAI_API_KEY:-}" ]; then
34
+ echo "Set OPENAI_API_KEY in your environment or in .env.local before running this script."
35
+ exit 1
36
+ fi
37
+
38
+ echo "Starting ChatKit backend on http://127.0.0.1:8000 ..."
39
+ exec uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
40
+
chatkit/frontend/.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .DS_Store
4
+ .next/
5
+ npm-debug.log*
6
+ *.tsbuildinfo
7
+ next-env.d.ts
chatkit/frontend/eslint.config.mjs ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "@typescript-eslint/eslint-plugin";
6
+ import tsParser from "@typescript-eslint/parser";
7
+
8
+ export default [
9
+ {
10
+ ignores: ["node_modules/**", "dist/**", ".next/**"],
11
+ },
12
+ js.configs.recommended,
13
+ {
14
+ files: ["**/*.{ts,tsx}"],
15
+ languageOptions: {
16
+ parser: tsParser,
17
+ parserOptions: {
18
+ projectService: true,
19
+ ecmaFeatures: { jsx: true },
20
+ },
21
+ globals: { ...globals.browser, ...globals.node },
22
+ },
23
+ plugins: {
24
+ "@typescript-eslint": tseslint,
25
+ "react-hooks": reactHooks,
26
+ "react-refresh": reactRefresh,
27
+ },
28
+ rules: {
29
+ ...tseslint.configs["recommended-type-checked"].rules,
30
+ ...reactHooks.configs.recommended.rules,
31
+ "react-refresh/only-export-components": [
32
+ "warn",
33
+ { allowConstantExport: true },
34
+ ],
35
+ },
36
+ },
37
+ ];
chatkit/frontend/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AgentKit demo</title>
7
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
+ <script src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
15
+
chatkit/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
chatkit/frontend/package.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatkit-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint . --ext ts,tsx --max-warnings=0"
11
+ },
12
+ "engines": {
13
+ "node": ">=18.18",
14
+ "npm": ">=9"
15
+ },
16
+ "dependencies": {
17
+ "@openai/chatkit-react": ">=1.1.1 <2.0.0",
18
+ "react": "^19.2.0",
19
+ "react-dom": "^19.2.0"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.17.0",
23
+ "@tailwindcss/postcss": "^4",
24
+ "@types/node": "^22.9.4",
25
+ "@types/react": "^19.0.8",
26
+ "@types/react-dom": "^19.0.3",
27
+ "@typescript-eslint/eslint-plugin": "^8.16.0",
28
+ "@typescript-eslint/parser": "^8.16.0",
29
+ "@vitejs/plugin-react-swc": "^3.5.0",
30
+ "eslint": "^9.17.0",
31
+ "eslint-plugin-react-hooks": "^5.0.0",
32
+ "eslint-plugin-react-refresh": "^0.4.16",
33
+ "globals": "^15.12.0",
34
+ "postcss": "^8.4.47",
35
+ "tailwindcss": "^4",
36
+ "typescript": "^5.6.3",
37
+ "vite": "^7.1.9"
38
+ }
39
+ }
postcss.config.mjs → chatkit/frontend/postcss.config.mjs RENAMED
@@ -1,5 +1,7 @@
1
  const config = {
2
- plugins: ["@tailwindcss/postcss"],
 
 
3
  };
4
 
5
  export default config;
 
1
  const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
  };
6
 
7
  export default config;
{app → chatkit/frontend/public}/favicon.ico RENAMED
File without changes
chatkit/frontend/src/App.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChatKitPanel } from "./components/ChatKitPanel";
2
+
3
+ export default function App() {
4
+ return (
5
+ <main className="flex min-h-screen flex-col items-center justify-end bg-slate-100 dark:bg-slate-950">
6
+ <div className="mx-auto w-full max-w-5xl">
7
+ <ChatKitPanel />
8
+ </div>
9
+ </main>
10
+ );
11
+ }
chatkit/frontend/src/components/ChatKitPanel.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChatKit, useChatKit } from "@openai/chatkit-react";
2
+ import { CHATKIT_API_DOMAIN_KEY, CHATKIT_API_URL } from "../lib/config";
3
+
4
+ export function ChatKitPanel() {
5
+ const chatkit = useChatKit({
6
+ api: { url: CHATKIT_API_URL, domainKey: CHATKIT_API_DOMAIN_KEY },
7
+ composer: {
8
+ // File uploads are disabled for the demo backend.
9
+ attachments: { enabled: false },
10
+ },
11
+ });
12
+
13
+ return (
14
+ <div className="relative pb-8 flex h-[90vh] w-full rounded-2xl flex-col overflow-hidden bg-white shadow-sm transition-colors dark:bg-slate-900">
15
+ <ChatKit control={chatkit.control} className="block h-full w-full" />
16
+ </div>
17
+ );
18
+ }
app/globals.css → chatkit/frontend/src/index.css RENAMED
File without changes
chatkit/frontend/src/lib/config.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const readEnvString = (value: unknown): string | undefined =>
2
+ typeof value === "string" && value.trim().length > 0
3
+ ? value.trim()
4
+ : undefined;
5
+
6
+ export const CHATKIT_API_URL =
7
+ readEnvString(import.meta.env.VITE_CHATKIT_API_URL) ?? "/chatkit";
8
+
9
+ /**
10
+ * ChatKit requires a domain key at runtime. Use the local fallback while
11
+ * developing, and register a production domain key for deployment:
12
+ * https://platform.openai.com/settings/organization/security/domain-allowlist
13
+ */
14
+ export const CHATKIT_API_DOMAIN_KEY =
15
+ readEnvString(import.meta.env.VITE_CHATKIT_API_DOMAIN_KEY) ??
16
+ "domain_pk_localhost_dev";
chatkit/frontend/src/main.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+ import "./index.css";
5
+
6
+ const container = document.getElementById("root");
7
+ if (!container) {
8
+ throw new Error("Root element with id 'root' not found");
9
+ }
10
+
11
+ createRoot(container).render(
12
+ <StrictMode>
13
+ <App />
14
+ </StrictMode>
15
+ );
16
+
chatkit/frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ declare global {
4
+ interface ImportMetaEnv {
5
+ readonly VITE_CHATKIT_API_URL?: string;
6
+ readonly VITE_CHATKIT_API_DOMAIN_KEY?: string;
7
+ }
8
+
9
+ interface ImportMeta {
10
+ readonly env: ImportMetaEnv;
11
+ }
12
+ }
13
+
14
+ export {};
chatkit/frontend/tsconfig.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "esModuleInterop": true,
13
+ "jsx": "react-jsx"
14
+ },
15
+ "include": ["src"],
16
+ "references": [{ "path": "./tsconfig.node.json" }]
17
+ }
chatkit/frontend/tsconfig.node.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "resolveJsonModule": true,
7
+ "isolatedModules": true,
8
+ "types": ["node"]
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
chatkit/frontend/vite.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as path from "node:path";
2
+ import { defineConfig } from "vite";
3
+ import react from "@vitejs/plugin-react-swc";
4
+
5
+ const backendTarget = process.env.CHATKIT_API_BASE ?? "http://127.0.0.1:8000";
6
+
7
+ export default defineConfig({
8
+ // Allow env files to live one level above the frontend directory
9
+ envDir: path.resolve(__dirname, ".."),
10
+ plugins: [react()],
11
+ server: {
12
+ port: 3000,
13
+ host: "0.0.0.0",
14
+ proxy: {
15
+ "/chatkit": {
16
+ target: backendTarget,
17
+ changeOrigin: true,
18
+ },
19
+ },
20
+ },
21
+ });
chatkit/package-lock.json ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatkit",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "chatkit",
8
+ "devDependencies": {
9
+ "concurrently": "^9.1.2"
10
+ }
11
+ },
12
+ "node_modules/ansi-regex": {
13
+ "version": "5.0.1",
14
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
15
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
16
+ "dev": true,
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=8"
20
+ }
21
+ },
22
+ "node_modules/ansi-styles": {
23
+ "version": "4.3.0",
24
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
25
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "color-convert": "^2.0.1"
30
+ },
31
+ "engines": {
32
+ "node": ">=8"
33
+ },
34
+ "funding": {
35
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
36
+ }
37
+ },
38
+ "node_modules/chalk": {
39
+ "version": "4.1.2",
40
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
41
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
42
+ "dev": true,
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "ansi-styles": "^4.1.0",
46
+ "supports-color": "^7.1.0"
47
+ },
48
+ "engines": {
49
+ "node": ">=10"
50
+ },
51
+ "funding": {
52
+ "url": "https://github.com/chalk/chalk?sponsor=1"
53
+ }
54
+ },
55
+ "node_modules/chalk/node_modules/supports-color": {
56
+ "version": "7.2.0",
57
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
58
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
59
+ "dev": true,
60
+ "license": "MIT",
61
+ "dependencies": {
62
+ "has-flag": "^4.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=8"
66
+ }
67
+ },
68
+ "node_modules/cliui": {
69
+ "version": "8.0.1",
70
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
71
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
72
+ "dev": true,
73
+ "license": "ISC",
74
+ "dependencies": {
75
+ "string-width": "^4.2.0",
76
+ "strip-ansi": "^6.0.1",
77
+ "wrap-ansi": "^7.0.0"
78
+ },
79
+ "engines": {
80
+ "node": ">=12"
81
+ }
82
+ },
83
+ "node_modules/color-convert": {
84
+ "version": "2.0.1",
85
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
86
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
87
+ "dev": true,
88
+ "license": "MIT",
89
+ "dependencies": {
90
+ "color-name": "~1.1.4"
91
+ },
92
+ "engines": {
93
+ "node": ">=7.0.0"
94
+ }
95
+ },
96
+ "node_modules/color-name": {
97
+ "version": "1.1.4",
98
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
99
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
100
+ "dev": true,
101
+ "license": "MIT"
102
+ },
103
+ "node_modules/concurrently": {
104
+ "version": "9.2.1",
105
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
106
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
107
+ "dev": true,
108
+ "license": "MIT",
109
+ "dependencies": {
110
+ "chalk": "4.1.2",
111
+ "rxjs": "7.8.2",
112
+ "shell-quote": "1.8.3",
113
+ "supports-color": "8.1.1",
114
+ "tree-kill": "1.2.2",
115
+ "yargs": "17.7.2"
116
+ },
117
+ "bin": {
118
+ "conc": "dist/bin/concurrently.js",
119
+ "concurrently": "dist/bin/concurrently.js"
120
+ },
121
+ "engines": {
122
+ "node": ">=18"
123
+ },
124
+ "funding": {
125
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
126
+ }
127
+ },
128
+ "node_modules/emoji-regex": {
129
+ "version": "8.0.0",
130
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
131
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
132
+ "dev": true,
133
+ "license": "MIT"
134
+ },
135
+ "node_modules/escalade": {
136
+ "version": "3.2.0",
137
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
138
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
139
+ "dev": true,
140
+ "license": "MIT",
141
+ "engines": {
142
+ "node": ">=6"
143
+ }
144
+ },
145
+ "node_modules/get-caller-file": {
146
+ "version": "2.0.5",
147
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
148
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
149
+ "dev": true,
150
+ "license": "ISC",
151
+ "engines": {
152
+ "node": "6.* || 8.* || >= 10.*"
153
+ }
154
+ },
155
+ "node_modules/has-flag": {
156
+ "version": "4.0.0",
157
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
158
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
159
+ "dev": true,
160
+ "license": "MIT",
161
+ "engines": {
162
+ "node": ">=8"
163
+ }
164
+ },
165
+ "node_modules/is-fullwidth-code-point": {
166
+ "version": "3.0.0",
167
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
168
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
169
+ "dev": true,
170
+ "license": "MIT",
171
+ "engines": {
172
+ "node": ">=8"
173
+ }
174
+ },
175
+ "node_modules/require-directory": {
176
+ "version": "2.1.1",
177
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
178
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
179
+ "dev": true,
180
+ "license": "MIT",
181
+ "engines": {
182
+ "node": ">=0.10.0"
183
+ }
184
+ },
185
+ "node_modules/rxjs": {
186
+ "version": "7.8.2",
187
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
188
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
189
+ "dev": true,
190
+ "license": "Apache-2.0",
191
+ "dependencies": {
192
+ "tslib": "^2.1.0"
193
+ }
194
+ },
195
+ "node_modules/shell-quote": {
196
+ "version": "1.8.3",
197
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
198
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
199
+ "dev": true,
200
+ "license": "MIT",
201
+ "engines": {
202
+ "node": ">= 0.4"
203
+ },
204
+ "funding": {
205
+ "url": "https://github.com/sponsors/ljharb"
206
+ }
207
+ },
208
+ "node_modules/string-width": {
209
+ "version": "4.2.3",
210
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
211
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
212
+ "dev": true,
213
+ "license": "MIT",
214
+ "dependencies": {
215
+ "emoji-regex": "^8.0.0",
216
+ "is-fullwidth-code-point": "^3.0.0",
217
+ "strip-ansi": "^6.0.1"
218
+ },
219
+ "engines": {
220
+ "node": ">=8"
221
+ }
222
+ },
223
+ "node_modules/strip-ansi": {
224
+ "version": "6.0.1",
225
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
226
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
227
+ "dev": true,
228
+ "license": "MIT",
229
+ "dependencies": {
230
+ "ansi-regex": "^5.0.1"
231
+ },
232
+ "engines": {
233
+ "node": ">=8"
234
+ }
235
+ },
236
+ "node_modules/supports-color": {
237
+ "version": "8.1.1",
238
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
239
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
240
+ "dev": true,
241
+ "license": "MIT",
242
+ "dependencies": {
243
+ "has-flag": "^4.0.0"
244
+ },
245
+ "engines": {
246
+ "node": ">=10"
247
+ },
248
+ "funding": {
249
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
250
+ }
251
+ },
252
+ "node_modules/tree-kill": {
253
+ "version": "1.2.2",
254
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
255
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
256
+ "dev": true,
257
+ "license": "MIT",
258
+ "bin": {
259
+ "tree-kill": "cli.js"
260
+ }
261
+ },
262
+ "node_modules/tslib": {
263
+ "version": "2.8.1",
264
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
265
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
266
+ "dev": true,
267
+ "license": "0BSD"
268
+ },
269
+ "node_modules/wrap-ansi": {
270
+ "version": "7.0.0",
271
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
272
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
273
+ "dev": true,
274
+ "license": "MIT",
275
+ "dependencies": {
276
+ "ansi-styles": "^4.0.0",
277
+ "string-width": "^4.1.0",
278
+ "strip-ansi": "^6.0.0"
279
+ },
280
+ "engines": {
281
+ "node": ">=10"
282
+ },
283
+ "funding": {
284
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
285
+ }
286
+ },
287
+ "node_modules/y18n": {
288
+ "version": "5.0.8",
289
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
290
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
291
+ "dev": true,
292
+ "license": "ISC",
293
+ "engines": {
294
+ "node": ">=10"
295
+ }
296
+ },
297
+ "node_modules/yargs": {
298
+ "version": "17.7.2",
299
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
300
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
301
+ "dev": true,
302
+ "license": "MIT",
303
+ "dependencies": {
304
+ "cliui": "^8.0.1",
305
+ "escalade": "^3.1.1",
306
+ "get-caller-file": "^2.0.5",
307
+ "require-directory": "^2.1.1",
308
+ "string-width": "^4.2.3",
309
+ "y18n": "^5.0.5",
310
+ "yargs-parser": "^21.1.1"
311
+ },
312
+ "engines": {
313
+ "node": ">=12"
314
+ }
315
+ },
316
+ "node_modules/yargs-parser": {
317
+ "version": "21.1.1",
318
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
319
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
320
+ "dev": true,
321
+ "license": "ISC",
322
+ "engines": {
323
+ "node": ">=12"
324
+ }
325
+ }
326
+ }
327
+ }
chatkit/package.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chatkit",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "concurrently --kill-others-on-fail --names backend,frontend \"npm run backend\" \"npm run frontend\"",
6
+ "frontend": "npm --prefix frontend install && npm --prefix frontend run dev",
7
+ "backend": "./backend/scripts/run.sh"
8
+ },
9
+ "devDependencies": {
10
+ "concurrently": "^9.1.2"
11
+ }
12
+ }
components/ChatKitPanel.tsx DELETED
@@ -1,418 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useEffect, useRef, useState } from "react";
4
- import { ChatKit, useChatKit } from "@openai/chatkit-react";
5
- import {
6
- STARTER_PROMPTS,
7
- PLACEHOLDER_INPUT,
8
- GREETING,
9
- CREATE_SESSION_ENDPOINT,
10
- WORKFLOW_ID,
11
- getThemeConfig,
12
- } from "@/lib/config";
13
- import { ErrorOverlay } from "./ErrorOverlay";
14
- import type { ColorScheme } from "@/hooks/useColorScheme";
15
-
16
- export type FactAction = {
17
- type: "save";
18
- factId: string;
19
- factText: string;
20
- };
21
-
22
- type ChatKitPanelProps = {
23
- theme: ColorScheme;
24
- onWidgetAction: (action: FactAction) => Promise<void>;
25
- onResponseEnd: () => void;
26
- onThemeRequest: (scheme: ColorScheme) => void;
27
- };
28
-
29
- type ErrorState = {
30
- script: string | null;
31
- session: string | null;
32
- integration: string | null;
33
- retryable: boolean;
34
- };
35
-
36
- const isBrowser = typeof window !== "undefined";
37
- const isDev = process.env.NODE_ENV !== "production";
38
-
39
- const createInitialErrors = (): ErrorState => ({
40
- script: null,
41
- session: null,
42
- integration: null,
43
- retryable: false,
44
- });
45
-
46
- export function ChatKitPanel({
47
- theme,
48
- onWidgetAction,
49
- onResponseEnd,
50
- onThemeRequest,
51
- }: ChatKitPanelProps) {
52
- const processedFacts = useRef(new Set<string>());
53
- const [errors, setErrors] = useState<ErrorState>(() => createInitialErrors());
54
- const [isInitializingSession, setIsInitializingSession] = useState(true);
55
- const isMountedRef = useRef(true);
56
- const [scriptStatus, setScriptStatus] = useState<
57
- "pending" | "ready" | "error"
58
- >(() =>
59
- isBrowser && window.customElements?.get("openai-chatkit")
60
- ? "ready"
61
- : "pending"
62
- );
63
- const [widgetInstanceKey, setWidgetInstanceKey] = useState(0);
64
-
65
- const setErrorState = useCallback((updates: Partial<ErrorState>) => {
66
- setErrors((current) => ({ ...current, ...updates }));
67
- }, []);
68
-
69
- useEffect(() => {
70
- return () => {
71
- isMountedRef.current = false;
72
- };
73
- }, []);
74
-
75
- useEffect(() => {
76
- if (!isBrowser) {
77
- return;
78
- }
79
-
80
- let timeoutId: number | undefined;
81
-
82
- const handleLoaded = () => {
83
- if (!isMountedRef.current) {
84
- return;
85
- }
86
- setScriptStatus("ready");
87
- setErrorState({ script: null });
88
- };
89
-
90
- const handleError = (event: Event) => {
91
- console.error("Failed to load chatkit.js for some reason", event);
92
- if (!isMountedRef.current) {
93
- return;
94
- }
95
- setScriptStatus("error");
96
- const detail = (event as CustomEvent<unknown>)?.detail ?? "unknown error";
97
- setErrorState({ script: `Error: ${detail}`, retryable: false });
98
- setIsInitializingSession(false);
99
- };
100
-
101
- window.addEventListener("chatkit-script-loaded", handleLoaded);
102
- window.addEventListener(
103
- "chatkit-script-error",
104
- handleError as EventListener
105
- );
106
-
107
- if (window.customElements?.get("openai-chatkit")) {
108
- handleLoaded();
109
- } else if (scriptStatus === "pending") {
110
- timeoutId = window.setTimeout(() => {
111
- if (!window.customElements?.get("openai-chatkit")) {
112
- handleError(
113
- new CustomEvent("chatkit-script-error", {
114
- detail:
115
- "ChatKit web component is unavailable. Verify that the script URL is reachable.",
116
- })
117
- );
118
- }
119
- }, 5000);
120
- }
121
-
122
- return () => {
123
- window.removeEventListener("chatkit-script-loaded", handleLoaded);
124
- window.removeEventListener(
125
- "chatkit-script-error",
126
- handleError as EventListener
127
- );
128
- if (timeoutId) {
129
- window.clearTimeout(timeoutId);
130
- }
131
- };
132
- }, [scriptStatus, setErrorState]);
133
-
134
- const isWorkflowConfigured = Boolean(
135
- WORKFLOW_ID && !WORKFLOW_ID.startsWith("wf_replace")
136
- );
137
-
138
- useEffect(() => {
139
- if (!isWorkflowConfigured && isMountedRef.current) {
140
- setErrorState({
141
- session: "Set NEXT_PUBLIC_CHATKIT_WORKFLOW_ID in your .env.local file.",
142
- retryable: false,
143
- });
144
- setIsInitializingSession(false);
145
- }
146
- }, [isWorkflowConfigured, setErrorState]);
147
-
148
- const handleResetChat = useCallback(() => {
149
- processedFacts.current.clear();
150
- if (isBrowser) {
151
- setScriptStatus(
152
- window.customElements?.get("openai-chatkit") ? "ready" : "pending"
153
- );
154
- }
155
- setIsInitializingSession(true);
156
- setErrors(createInitialErrors());
157
- setWidgetInstanceKey((prev) => prev + 1);
158
- }, []);
159
-
160
- const getClientSecret = useCallback(
161
- async (currentSecret: string | null) => {
162
- if (isDev) {
163
- console.info("[ChatKitPanel] getClientSecret invoked", {
164
- currentSecretPresent: Boolean(currentSecret),
165
- workflowId: WORKFLOW_ID,
166
- endpoint: CREATE_SESSION_ENDPOINT,
167
- });
168
- }
169
-
170
- if (!isWorkflowConfigured) {
171
- const detail =
172
- "Set NEXT_PUBLIC_CHATKIT_WORKFLOW_ID in your .env.local file.";
173
- if (isMountedRef.current) {
174
- setErrorState({ session: detail, retryable: false });
175
- setIsInitializingSession(false);
176
- }
177
- throw new Error(detail);
178
- }
179
-
180
- if (isMountedRef.current) {
181
- if (!currentSecret) {
182
- setIsInitializingSession(true);
183
- }
184
- setErrorState({ session: null, integration: null, retryable: false });
185
- }
186
-
187
- try {
188
- const response = await fetch(CREATE_SESSION_ENDPOINT, {
189
- method: "POST",
190
- headers: {
191
- "Content-Type": "application/json",
192
- },
193
- body: JSON.stringify({
194
- workflow: { id: WORKFLOW_ID },
195
- chatkit_configuration: {
196
- // enable attachments
197
- file_upload: {
198
- enabled: true,
199
- },
200
- },
201
- }),
202
- });
203
-
204
- const raw = await response.text();
205
-
206
- if (isDev) {
207
- console.info("[ChatKitPanel] createSession response", {
208
- status: response.status,
209
- ok: response.ok,
210
- bodyPreview: raw.slice(0, 1600),
211
- });
212
- }
213
-
214
- let data: Record<string, unknown> = {};
215
- if (raw) {
216
- try {
217
- data = JSON.parse(raw) as Record<string, unknown>;
218
- } catch (parseError) {
219
- console.error(
220
- "Failed to parse create-session response",
221
- parseError
222
- );
223
- }
224
- }
225
-
226
- if (!response.ok) {
227
- const detail = extractErrorDetail(data, response.statusText);
228
- console.error("Create session request failed", {
229
- status: response.status,
230
- body: data,
231
- });
232
- throw new Error(detail);
233
- }
234
-
235
- const clientSecret = data?.client_secret as string | undefined;
236
- if (!clientSecret) {
237
- throw new Error("Missing client secret in response");
238
- }
239
-
240
- if (isMountedRef.current) {
241
- setErrorState({ session: null, integration: null });
242
- }
243
-
244
- return clientSecret;
245
- } catch (error) {
246
- console.error("Failed to create ChatKit session", error);
247
- const detail =
248
- error instanceof Error
249
- ? error.message
250
- : "Unable to start ChatKit session.";
251
- if (isMountedRef.current) {
252
- setErrorState({ session: detail, retryable: false });
253
- }
254
- throw error instanceof Error ? error : new Error(detail);
255
- } finally {
256
- if (isMountedRef.current && !currentSecret) {
257
- setIsInitializingSession(false);
258
- }
259
- }
260
- },
261
- [isWorkflowConfigured, setErrorState]
262
- );
263
-
264
- const chatkit = useChatKit({
265
- api: { getClientSecret },
266
- theme: {
267
- colorScheme: theme,
268
- ...getThemeConfig(theme),
269
- },
270
- startScreen: {
271
- greeting: GREETING,
272
- prompts: STARTER_PROMPTS,
273
- },
274
- composer: {
275
- placeholder: PLACEHOLDER_INPUT,
276
- attachments: {
277
- // Enable attachments
278
- enabled: true,
279
- },
280
- },
281
- threadItemActions: {
282
- feedback: false,
283
- },
284
- onClientTool: async (invocation: {
285
- name: string;
286
- params: Record<string, unknown>;
287
- }) => {
288
- if (invocation.name === "switch_theme") {
289
- const requested = invocation.params.theme;
290
- if (requested === "light" || requested === "dark") {
291
- if (isDev) {
292
- console.debug("[ChatKitPanel] switch_theme", requested);
293
- }
294
- onThemeRequest(requested);
295
- return { success: true };
296
- }
297
- return { success: false };
298
- }
299
-
300
- if (invocation.name === "record_fact") {
301
- const id = String(invocation.params.fact_id ?? "");
302
- const text = String(invocation.params.fact_text ?? "");
303
- if (!id || processedFacts.current.has(id)) {
304
- return { success: true };
305
- }
306
- processedFacts.current.add(id);
307
- void onWidgetAction({
308
- type: "save",
309
- factId: id,
310
- factText: text.replace(/\s+/g, " ").trim(),
311
- });
312
- return { success: true };
313
- }
314
-
315
- return { success: false };
316
- },
317
- onResponseEnd: () => {
318
- onResponseEnd();
319
- },
320
- onResponseStart: () => {
321
- setErrorState({ integration: null, retryable: false });
322
- },
323
- onThreadChange: () => {
324
- processedFacts.current.clear();
325
- },
326
- onError: ({ error }: { error: unknown }) => {
327
- // Note that Chatkit UI handles errors for your users.
328
- // Thus, your app code doesn't need to display errors on UI.
329
- console.error("ChatKit error", error);
330
- },
331
- });
332
-
333
- const activeError = errors.session ?? errors.integration;
334
- const blockingError = errors.script ?? activeError;
335
-
336
- if (isDev) {
337
- console.debug("[ChatKitPanel] render state", {
338
- isInitializingSession,
339
- hasControl: Boolean(chatkit.control),
340
- scriptStatus,
341
- hasError: Boolean(blockingError),
342
- workflowId: WORKFLOW_ID,
343
- });
344
- }
345
-
346
- return (
347
- <div className="relative pb-8 flex h-[90vh] w-full rounded-2xl flex-col overflow-hidden bg-white shadow-sm transition-colors dark:bg-slate-900">
348
- <ChatKit
349
- key={widgetInstanceKey}
350
- control={chatkit.control}
351
- className={
352
- blockingError || isInitializingSession
353
- ? "pointer-events-none opacity-0"
354
- : "block h-full w-full"
355
- }
356
- />
357
- <ErrorOverlay
358
- error={blockingError}
359
- fallbackMessage={
360
- blockingError || !isInitializingSession
361
- ? null
362
- : "Loading assistant session..."
363
- }
364
- onRetry={blockingError && errors.retryable ? handleResetChat : null}
365
- retryLabel="Restart chat"
366
- />
367
- </div>
368
- );
369
- }
370
-
371
- function extractErrorDetail(
372
- payload: Record<string, unknown> | undefined,
373
- fallback: string
374
- ): string {
375
- if (!payload) {
376
- return fallback;
377
- }
378
-
379
- const error = payload.error;
380
- if (typeof error === "string") {
381
- return error;
382
- }
383
-
384
- if (
385
- error &&
386
- typeof error === "object" &&
387
- "message" in error &&
388
- typeof (error as { message?: unknown }).message === "string"
389
- ) {
390
- return (error as { message: string }).message;
391
- }
392
-
393
- const details = payload.details;
394
- if (typeof details === "string") {
395
- return details;
396
- }
397
-
398
- if (details && typeof details === "object" && "error" in details) {
399
- const nestedError = (details as { error?: unknown }).error;
400
- if (typeof nestedError === "string") {
401
- return nestedError;
402
- }
403
- if (
404
- nestedError &&
405
- typeof nestedError === "object" &&
406
- "message" in nestedError &&
407
- typeof (nestedError as { message?: unknown }).message === "string"
408
- ) {
409
- return (nestedError as { message: string }).message;
410
- }
411
- }
412
-
413
- if (typeof payload.message === "string") {
414
- return payload.message;
415
- }
416
-
417
- return fallback;
418
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ErrorOverlay.tsx DELETED
@@ -1,44 +0,0 @@
1
- "use client";
2
-
3
- import type { ReactNode } from "react";
4
-
5
- type ErrorOverlayProps = {
6
- error: string | null;
7
- fallbackMessage?: ReactNode;
8
- onRetry?: (() => void) | null;
9
- retryLabel?: string;
10
- };
11
-
12
- export function ErrorOverlay({
13
- error,
14
- fallbackMessage,
15
- onRetry,
16
- retryLabel,
17
- }: ErrorOverlayProps) {
18
- if (!error && !fallbackMessage) {
19
- return null;
20
- }
21
-
22
- const content = error ?? fallbackMessage;
23
-
24
- if (!content) {
25
- return null;
26
- }
27
-
28
- return (
29
- <div className="pointer-events-none absolute inset-0 z-10 flex h-full w-full flex-col justify-center rounded-[inherit] bg-white/85 p-6 text-center backdrop-blur dark:bg-slate-900/90">
30
- <div className="pointer-events-auto mx-auto w-full max-w-md rounded-xl bg-white px-6 py-4 text-lg font-medium text-slate-700 dark:bg-transparent dark:text-slate-100">
31
- <div>{content}</div>
32
- {error && onRetry ? (
33
- <button
34
- type="button"
35
- className="mt-4 inline-flex items-center justify-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-none transition hover:bg-slate-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-500 focus-visible:ring-offset-2 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200"
36
- onClick={onRetry}
37
- >
38
- {retryLabel ?? "Restart chat"}
39
- </button>
40
- ) : null}
41
- </div>
42
- </div>
43
- );
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eslint.config.mjs DELETED
@@ -1,25 +0,0 @@
1
- import { dirname } from "path";
2
- import { fileURLToPath } from "url";
3
- import { FlatCompat } from "@eslint/eslintrc";
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = dirname(__filename);
7
-
8
- const compat = new FlatCompat({
9
- baseDirectory: __dirname,
10
- });
11
-
12
- const eslintConfig = [
13
- ...compat.extends("next/core-web-vitals", "next/typescript"),
14
- {
15
- ignores: [
16
- "node_modules/**",
17
- ".next/**",
18
- "out/**",
19
- "build/**",
20
- "next-env.d.ts",
21
- ],
22
- },
23
- ];
24
-
25
- export default eslintConfig;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hooks/useColorScheme.ts DELETED
@@ -1,178 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
4
-
5
- export type ColorScheme = "light" | "dark";
6
- export type ColorSchemePreference = ColorScheme | "system";
7
-
8
- const STORAGE_KEY = "chatkit-color-scheme";
9
- const PREFERS_DARK_QUERY = "(prefers-color-scheme: dark)";
10
-
11
- type MediaQueryCallback = (event: MediaQueryListEvent) => void;
12
-
13
- function getMediaQuery(): MediaQueryList | null {
14
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
15
- return null;
16
- }
17
- try {
18
- return window.matchMedia(PREFERS_DARK_QUERY);
19
- } catch (error) {
20
- if (process.env.NODE_ENV !== "production") {
21
- console.warn("[useColorScheme] matchMedia failed", error);
22
- }
23
- return null;
24
- }
25
- }
26
-
27
- function getSystemSnapshot(): ColorScheme {
28
- const media = getMediaQuery();
29
- return media?.matches ? "dark" : "light";
30
- }
31
-
32
- function getServerSnapshot(): ColorScheme {
33
- return "light";
34
- }
35
-
36
- function subscribeSystem(listener: () => void): () => void {
37
- const media = getMediaQuery();
38
- if (!media) {
39
- return () => { };
40
- }
41
-
42
- const handler: MediaQueryCallback = () => listener();
43
-
44
- if (typeof media.addEventListener === "function") {
45
- media.addEventListener("change", handler);
46
- return () => media.removeEventListener("change", handler);
47
- }
48
-
49
- // Fallback for older browsers or environments.
50
- if (typeof media.addListener === "function") {
51
- media.addListener(handler);
52
- return () => media.removeListener(handler);
53
- }
54
-
55
- return () => { };
56
- }
57
-
58
- function readStoredPreference(): ColorSchemePreference | null {
59
- if (typeof window === "undefined") {
60
- return null;
61
- }
62
- try {
63
- const raw = window.localStorage.getItem(STORAGE_KEY);
64
- if (raw === "light" || raw === "dark") {
65
- return raw;
66
- }
67
- return raw === "system" ? "system" : null;
68
- } catch (error) {
69
- if (process.env.NODE_ENV !== "production") {
70
- console.warn("[useColorScheme] Failed to read preference", error);
71
- }
72
- return null;
73
- }
74
- }
75
-
76
- function persistPreference(preference: ColorSchemePreference): void {
77
- if (typeof window === "undefined") {
78
- return;
79
- }
80
- try {
81
- if (preference === "system") {
82
- window.localStorage.removeItem(STORAGE_KEY);
83
- } else {
84
- window.localStorage.setItem(STORAGE_KEY, preference);
85
- }
86
- } catch (error) {
87
- if (process.env.NODE_ENV !== "production") {
88
- console.warn("[useColorScheme] Failed to persist preference", error);
89
- }
90
- }
91
- }
92
-
93
- function applyDocumentScheme(scheme: ColorScheme): void {
94
- if (typeof document === "undefined") {
95
- return;
96
- }
97
- const root = document.documentElement;
98
- root.dataset.colorScheme = scheme;
99
- root.classList.toggle("dark", scheme === "dark");
100
- root.style.colorScheme = scheme;
101
- }
102
-
103
- type UseColorSchemeResult = {
104
- scheme: ColorScheme;
105
- preference: ColorSchemePreference;
106
- setScheme: (scheme: ColorScheme) => void;
107
- setPreference: (preference: ColorSchemePreference) => void;
108
- resetPreference: () => void;
109
- };
110
-
111
- function useSystemColorScheme(): ColorScheme {
112
- return useSyncExternalStore(subscribeSystem, getSystemSnapshot, getServerSnapshot);
113
- }
114
-
115
- export function useColorScheme(
116
- initialPreference: ColorSchemePreference = "system"
117
- ): UseColorSchemeResult {
118
- const systemScheme = useSystemColorScheme();
119
-
120
- const [preference, setPreferenceState] = useState<ColorSchemePreference>(() => {
121
- if (typeof window === "undefined") {
122
- return initialPreference;
123
- }
124
- return readStoredPreference() ?? initialPreference;
125
- });
126
-
127
- const scheme = useMemo<ColorScheme>(
128
- () => (preference === "system" ? systemScheme : preference),
129
- [preference, systemScheme]
130
- );
131
-
132
- useEffect(() => {
133
- persistPreference(preference);
134
- }, [preference]);
135
-
136
- useEffect(() => {
137
- applyDocumentScheme(scheme);
138
- }, [scheme]);
139
-
140
- useEffect(() => {
141
- if (typeof window === "undefined") {
142
- return;
143
- }
144
-
145
- const handleStorage = (event: StorageEvent) => {
146
- if (event.key !== STORAGE_KEY) {
147
- return;
148
- }
149
- setPreferenceState((current) => {
150
- const stored = readStoredPreference();
151
- return stored ?? current;
152
- });
153
- };
154
-
155
- window.addEventListener("storage", handleStorage);
156
- return () => window.removeEventListener("storage", handleStorage);
157
- }, []);
158
-
159
- const setPreference = useCallback((next: ColorSchemePreference) => {
160
- setPreferenceState(next);
161
- }, []);
162
-
163
- const setScheme = useCallback((next: ColorScheme) => {
164
- setPreferenceState(next);
165
- }, []);
166
-
167
- const resetPreference = useCallback(() => {
168
- setPreferenceState("system");
169
- }, []);
170
-
171
- return {
172
- scheme,
173
- preference,
174
- setScheme,
175
- setPreference,
176
- resetPreference,
177
- };
178
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lib/config.ts DELETED
@@ -1,35 +0,0 @@
1
- import { ColorScheme, StartScreenPrompt, ThemeOption } from "@openai/chatkit";
2
-
3
- export const WORKFLOW_ID =
4
- process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID?.trim() ?? "";
5
-
6
- export const CREATE_SESSION_ENDPOINT = "/api/create-session";
7
-
8
- export const STARTER_PROMPTS: StartScreenPrompt[] = [
9
- {
10
- label: "What can you do?",
11
- prompt: "What can you do?",
12
- icon: "circle-question",
13
- },
14
- ];
15
-
16
- export const PLACEHOLDER_INPUT = "Ask anything...";
17
-
18
- export const GREETING = "How can I help you today?";
19
-
20
- export const getThemeConfig = (theme: ColorScheme): ThemeOption => ({
21
- color: {
22
- grayscale: {
23
- hue: 220,
24
- tint: 6,
25
- shade: theme === "dark" ? -1 : -4,
26
- },
27
- accent: {
28
- primary: theme === "dark" ? "#f1f5f9" : "#0f172a",
29
- level: 1,
30
- },
31
- },
32
- radius: "round",
33
- // Add other theme options here
34
- // chatkit.studio/playground to explore config options
35
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.env.example → managed-chatkit/.env.example RENAMED
File without changes
.gitignore → managed-chatkit/.gitignore RENAMED
@@ -2,6 +2,7 @@
2
 
3
  # dependencies
4
  /node_modules
 
5
  /.pnp
6
  .pnp.*
7
  .yarn/*
@@ -13,12 +14,11 @@
13
  # testing
14
  /coverage
15
 
16
- # next.js
17
- /.next/
18
- /out/
19
-
20
  # production
21
- /build
 
 
 
22
 
23
  # misc
24
  .DS_Store
@@ -27,16 +27,15 @@
27
  # debug
28
  npm-debug.log*
29
  yarn-debug.log*
30
- yarn-error.log*
31
  .pnpm-debug.log*
32
 
33
- # env files (can opt-in for committing if needed)
34
  .env*
35
  !.env.example
36
-
37
- # vercel
38
- .vercel
39
 
40
  # typescript
41
- *.tsbuildinfo
42
- next-env.d.ts
 
2
 
3
  # dependencies
4
  /node_modules
5
+ /frontend/node_modules
6
  /.pnp
7
  .pnp.*
8
  .yarn/*
 
14
  # testing
15
  /coverage
16
 
 
 
 
 
17
  # production
18
+ build/
19
+ dist/
20
+ /frontend/dist/
21
+ /frontend/.vite
22
 
23
  # misc
24
  .DS_Store
 
27
  # debug
28
  npm-debug.log*
29
  yarn-debug.log*
30
+ yarn-error.log*g
31
  .pnpm-debug.log*
32
 
33
+ # env files
34
  .env*
35
  !.env.example
36
+ /backend/.env*
37
+ /backend/.venv
 
38
 
39
  # typescript
40
+ *.tsbuildinfog
41
+ /backend/.ruff_cache
managed-chatkit/README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Managed ChatKit starter
2
+
3
+ Vite + React UI that talks to a FastAPI session backend for creating ChatKit
4
+ workflow sessions.
5
+
6
+ ## Quick start
7
+
8
+ ```bash
9
+ npm install # installs root deps (concurrently)
10
+ npm run dev # runs FastAPI on :8000 and Vite on :3000
11
+ ```
12
+
13
+ What happens:
14
+
15
+ - `npm run dev` runs the backend via `backend/scripts/run.sh` (FastAPI +
16
+ uvicorn) and the frontend via `npm --prefix frontend run dev`.
17
+ - The backend exposes `/api/create-session`, exchanging your workflow id and
18
+ `OPENAI_API_KEY` for a ChatKit client secret. The Vite dev server proxies
19
+ `/api/*` to `127.0.0.1:8000`.
20
+
21
+ ## Required environment
22
+
23
+ - `OPENAI_API_KEY`
24
+ - `VITE_CHATKIT_WORKFLOW_ID`
25
+ - (optional) `CHATKIT_API_BASE` or `VITE_CHATKIT_API_BASE` (defaults to `https://api.openai.com`)
26
+ - (optional) `VITE_API_URL` (override the dev proxy target for `/api`)
27
+
28
+ Set the env vars in your shell (or process manager) before running. Use a
29
+ workflow id from Agent Builder (starts with `wf_...`) and an API key from the
30
+ same project and organization.
31
+
32
+ ## Customize
33
+
34
+ - UI: `frontend/src/components/ChatKitPanel.tsx`
35
+ - Session logic: `backend/app/main.py`
managed-chatkit/backend/.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .env
6
+ .ruff_cache/
7
+ .pytest_cache/
8
+ .coverage/
9
+ *.log
managed-chatkit/backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Managed ChatKit backend package."""
managed-chatkit/backend/app/main.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI entrypoint for exchanging workflow ids for ChatKit client secrets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import uuid
8
+ from typing import Any, Mapping
9
+
10
+ import httpx
11
+ from fastapi import FastAPI, Request
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.responses import JSONResponse
14
+
15
+ DEFAULT_CHATKIT_BASE = "https://api.openai.com"
16
+ SESSION_COOKIE_NAME = "chatkit_session_id"
17
+ SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30 # 30 days
18
+
19
+ app = FastAPI(title="Managed ChatKit Session API")
20
+
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+
30
+ @app.get("/health")
31
+ async def health() -> Mapping[str, str]:
32
+ return {"status": "ok"}
33
+
34
+
35
+ @app.post("/api/create-session")
36
+ async def create_session(request: Request) -> JSONResponse:
37
+ """Exchange a workflow id for a ChatKit client secret."""
38
+ api_key = os.getenv("OPENAI_API_KEY")
39
+ if not api_key:
40
+ return respond({"error": "Missing OPENAI_API_KEY environment variable"}, 500)
41
+
42
+ body = await read_json_body(request)
43
+ workflow_id = resolve_workflow_id(body)
44
+ if not workflow_id:
45
+ return respond({"error": "Missing workflow id"}, 400)
46
+
47
+ user_id, cookie_value = resolve_user(request.cookies)
48
+ api_base = chatkit_api_base()
49
+
50
+ try:
51
+ async with httpx.AsyncClient(base_url=api_base, timeout=10.0) as client:
52
+ upstream = await client.post(
53
+ "/v1/chatkit/sessions",
54
+ headers={
55
+ "Authorization": f"Bearer {api_key}",
56
+ "OpenAI-Beta": "chatkit_beta=v1",
57
+ "Content-Type": "application/json",
58
+ },
59
+ json={"workflow": {"id": workflow_id}, "user": user_id},
60
+ )
61
+ except httpx.RequestError as error:
62
+ return respond(
63
+ {"error": f"Failed to reach ChatKit API: {error}"},
64
+ 502,
65
+ cookie_value,
66
+ )
67
+
68
+ payload = parse_json(upstream)
69
+ if not upstream.is_success:
70
+ message = None
71
+ if isinstance(payload, Mapping):
72
+ message = payload.get("error")
73
+ message = message or upstream.reason_phrase or "Failed to create session"
74
+ return respond({"error": message}, upstream.status_code, cookie_value)
75
+
76
+ client_secret = None
77
+ expires_after = None
78
+ if isinstance(payload, Mapping):
79
+ client_secret = payload.get("client_secret")
80
+ expires_after = payload.get("expires_after")
81
+
82
+ if not client_secret:
83
+ return respond(
84
+ {"error": "Missing client secret in response"},
85
+ 502,
86
+ cookie_value,
87
+ )
88
+
89
+ return respond(
90
+ {"client_secret": client_secret, "expires_after": expires_after},
91
+ 200,
92
+ cookie_value,
93
+ )
94
+
95
+
96
+ def respond(
97
+ payload: Mapping[str, Any], status_code: int, cookie_value: str | None = None
98
+ ) -> JSONResponse:
99
+ response = JSONResponse(payload, status_code=status_code)
100
+ if cookie_value:
101
+ response.set_cookie(
102
+ key=SESSION_COOKIE_NAME,
103
+ value=cookie_value,
104
+ max_age=SESSION_COOKIE_MAX_AGE_SECONDS,
105
+ httponly=True,
106
+ samesite="lax",
107
+ secure=is_prod(),
108
+ path="/",
109
+ )
110
+ return response
111
+
112
+
113
+ def is_prod() -> bool:
114
+ env = (os.getenv("ENVIRONMENT") or os.getenv("NODE_ENV") or "").lower()
115
+ return env == "production"
116
+
117
+
118
+ async def read_json_body(request: Request) -> Mapping[str, Any]:
119
+ raw = await request.body()
120
+ if not raw:
121
+ return {}
122
+ try:
123
+ parsed = json.loads(raw)
124
+ except json.JSONDecodeError:
125
+ return {}
126
+ return parsed if isinstance(parsed, Mapping) else {}
127
+
128
+
129
+ def resolve_workflow_id(body: Mapping[str, Any]) -> str | None:
130
+ workflow = body.get("workflow", {})
131
+ workflow_id = None
132
+ if isinstance(workflow, Mapping):
133
+ workflow_id = workflow.get("id")
134
+ workflow_id = workflow_id or body.get("workflowId")
135
+ env_workflow = os.getenv("CHATKIT_WORKFLOW_ID") or os.getenv(
136
+ "VITE_CHATKIT_WORKFLOW_ID"
137
+ )
138
+ if not workflow_id and env_workflow:
139
+ workflow_id = env_workflow
140
+ if workflow_id and isinstance(workflow_id, str) and workflow_id.strip():
141
+ return workflow_id.strip()
142
+ return None
143
+
144
+
145
+ def resolve_user(cookies: Mapping[str, str]) -> tuple[str, str | None]:
146
+ existing = cookies.get(SESSION_COOKIE_NAME)
147
+ if existing:
148
+ return existing, None
149
+ user_id = str(uuid.uuid4())
150
+ return user_id, user_id
151
+
152
+
153
+ def chatkit_api_base() -> str:
154
+ return (
155
+ os.getenv("CHATKIT_API_BASE")
156
+ or os.getenv("VITE_CHATKIT_API_BASE")
157
+ or DEFAULT_CHATKIT_BASE
158
+ )
159
+
160
+
161
+ def parse_json(response: httpx.Response) -> Mapping[str, Any]:
162
+ try:
163
+ parsed = response.json()
164
+ return parsed if isinstance(parsed, Mapping) else {}
165
+ except (json.JSONDecodeError, httpx.DecodingError):
166
+ return {}
managed-chatkit/backend/pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "managed-chatkit-backend"
3
+ version = "0.1.0"
4
+ description = "FastAPI backend for creating ChatKit workflow sessions"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "fastapi>=0.114,<0.116",
8
+ "httpx>=0.27,<0.28",
9
+ "uvicorn[standard]>=0.36,<0.37",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "ruff>=0.6.4,<0.7",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["setuptools>=68.0", "wheel"]
19
+ build-backend = "setuptools.build_meta"
20
+
managed-chatkit/backend/scripts/run.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Start the Managed ChatKit FastAPI backend.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
9
+
10
+ cd "$PROJECT_ROOT"
11
+
12
+ if [ ! -d ".venv" ]; then
13
+ echo "Creating virtual env in $PROJECT_ROOT/.venv ..."
14
+ python -m venv .venv
15
+ fi
16
+
17
+ source .venv/bin/activate
18
+
19
+ echo "Installing backend deps (editable) ..."
20
+ pip install -e . >/dev/null
21
+
22
+ # Load env vars from the repo's .env.local (if present) so OPENAI_API_KEY
23
+ # does not need to be exported manually.
24
+ ENV_FILE="$PROJECT_ROOT/../.env.local"
25
+ if [ -z "${OPENAI_API_KEY:-}" ] && [ -f "$ENV_FILE" ]; then
26
+ echo "Sourcing OPENAI_API_KEY from $ENV_FILE"
27
+ # shellcheck disable=SC1090
28
+ set -a
29
+ . "$ENV_FILE"
30
+ set +a
31
+ fi
32
+
33
+ if [ -z "${OPENAI_API_KEY:-}" ]; then
34
+ echo "Set OPENAI_API_KEY in your environment or in .env.local before running this script."
35
+ exit 1
36
+ fi
37
+
38
+ export PYTHONPATH="$PROJECT_ROOT${PYTHONPATH:+:$PYTHONPATH}"
39
+
40
+ echo "Starting Managed ChatKit backend on http://127.0.0.1:8000 ..."
41
+ exec uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
42
+
managed-chatkit/frontend/eslint.config.mjs ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "@typescript-eslint/eslint-plugin";
6
+ import tsParser from "@typescript-eslint/parser";
7
+ import { URL } from "node:url";
8
+
9
+ export default [
10
+ {
11
+ ignores: ["node_modules/**", "dist/**", ".next/**"],
12
+ },
13
+ js.configs.recommended,
14
+ {
15
+ files: ["src/**/*.{ts,tsx}"],
16
+ languageOptions: {
17
+ parser: tsParser,
18
+ parserOptions: {
19
+ project: ["./tsconfig.json"],
20
+ tsconfigRootDir: new URL(".", import.meta.url).pathname,
21
+ ecmaFeatures: { jsx: true },
22
+ },
23
+ globals: { ...globals.browser, ...globals.node },
24
+ },
25
+ plugins: {
26
+ "@typescript-eslint": tseslint,
27
+ "react-hooks": reactHooks,
28
+ "react-refresh": reactRefresh,
29
+ },
30
+ rules: {
31
+ ...tseslint.configs["recommended-type-checked"].rules,
32
+ ...reactHooks.configs.recommended.rules,
33
+ "react-refresh/only-export-components": [
34
+ "warn",
35
+ { allowConstantExport: true },
36
+ ],
37
+ },
38
+ },
39
+ {
40
+ files: ["vite.config.ts"],
41
+ languageOptions: {
42
+ parser: tsParser,
43
+ parserOptions: {
44
+ project: ["./tsconfig.node.json"],
45
+ tsconfigRootDir: new URL(".", import.meta.url).pathname,
46
+ },
47
+ globals: { ...globals.node },
48
+ },
49
+ plugins: {
50
+ "@typescript-eslint": tseslint,
51
+ },
52
+ rules: {
53
+ ...tseslint.configs.recommended.rules,
54
+ },
55
+ },
56
+ ];
managed-chatkit/frontend/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AgentKit demo</title>
7
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
+ <script src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"></script>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
15
+