Spaces:
Runtime error
Runtime error
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
- .github/workflows/ci.yml +59 -2
- README.md +5 -68
- app/App.tsx +0 -34
- app/api/create-session/route.ts +0 -283
- app/layout.tsx +0 -26
- app/page.tsx +0 -5
- chatkit/.env.example +3 -0
- chatkit/.gitignore +3 -0
- chatkit/README.md +32 -0
- chatkit/backend/.gitignore +9 -0
- chatkit/backend/app/__init__.py +1 -0
- chatkit/backend/app/assistant.py +31 -0
- chatkit/backend/app/main.py +35 -0
- chatkit/backend/app/memory_store.py +196 -0
- chatkit/backend/app/server.py +54 -0
- chatkit/backend/pyproject.toml +21 -0
- chatkit/backend/scripts/run.sh +40 -0
- chatkit/frontend/.gitignore +7 -0
- chatkit/frontend/eslint.config.mjs +37 -0
- chatkit/frontend/index.html +15 -0
- chatkit/frontend/package-lock.json +0 -0
- chatkit/frontend/package.json +39 -0
- postcss.config.mjs → chatkit/frontend/postcss.config.mjs +3 -1
- {app → chatkit/frontend/public}/favicon.ico +0 -0
- chatkit/frontend/src/App.tsx +11 -0
- chatkit/frontend/src/components/ChatKitPanel.tsx +18 -0
- app/globals.css → chatkit/frontend/src/index.css +0 -0
- chatkit/frontend/src/lib/config.ts +16 -0
- chatkit/frontend/src/main.tsx +16 -0
- chatkit/frontend/src/vite-env.d.ts +14 -0
- chatkit/frontend/tsconfig.json +17 -0
- chatkit/frontend/tsconfig.node.json +11 -0
- chatkit/frontend/vite.config.ts +21 -0
- chatkit/package-lock.json +327 -0
- chatkit/package.json +12 -0
- components/ChatKitPanel.tsx +0 -418
- components/ErrorOverlay.tsx +0 -44
- eslint.config.mjs +0 -25
- hooks/useColorScheme.ts +0 -178
- lib/config.ts +0 -35
- .env.example → managed-chatkit/.env.example +0 -0
- .gitignore → managed-chatkit/.gitignore +11 -12
- managed-chatkit/README.md +35 -0
- managed-chatkit/backend/.gitignore +9 -0
- managed-chatkit/backend/app/__init__.py +1 -0
- managed-chatkit/backend/app/main.py +166 -0
- managed-chatkit/backend/pyproject.toml +20 -0
- managed-chatkit/backend/scripts/run.sh +42 -0
- managed-chatkit/frontend/eslint.config.mjs +56 -0
- managed-chatkit/frontend/index.html +15 -0
.github/workflows/ci.yml
CHANGED
|
@@ -6,9 +6,62 @@ on:
|
|
| 6 |
pull_request:
|
| 7 |
|
| 8 |
jobs:
|
| 9 |
-
|
| 10 |
-
name:
|
| 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
|
| 2 |
|
| 3 |
-
|
| 4 |
-

|
| 5 |
-

|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 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:
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 34 |
.env*
|
| 35 |
!.env.example
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
.vercel
|
| 39 |
|
| 40 |
# typescript
|
| 41 |
-
*.
|
| 42 |
-
|
|
|
|
| 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 |
+
|