Kazuhiro Sera katia-openai Corwin commited on
Commit
548eca7
·
0 Parent(s):

Initial commit

Browse files

Co-authored-by: Katia Gil Guzman <katia@openai.com>
Co-authored-by: Corwin <corwin@openai.com>

.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ OPENAI_API_KEY=sk-proj-...
2
+ NEXT_PUBLIC_CHATKIT_WORKFLOW_ID=wf_...
.github/workflows/ci.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+
8
+ jobs:
9
+ build:
10
+ name: Install, Lint, and Build
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup Node.js
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
+
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ !.env.example
36
+
37
+ # vercel
38
+ .vercel
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChatKit Starter Template
2
+
3
+ This repository is the simplest way to bootstrap a [ChatKit](http://openai.github.io/chatkit-js/) application. It ships with a minimal Next.js UI, the ChatKit web component, and a ready-to-use session endpoint so you can experiment with OpenAI-hosted workflows built using [Agent Builder](https://platform.openai.com/agent-builder).
4
+
5
+ ## What You Get
6
+
7
+ - Next.js app with `<openai-chatkit>` web component and theming controls
8
+ - API endpoint for creating a session at [`app/api/create-session/route.ts`](app/api/create-session/route.ts)
9
+ - Quick examples for starter prompts, placeholder text, and greating message
10
+
11
+ ## Getting Started
12
+
13
+ Follow every step below to run the app locally and configure it for your preferred backend.
14
+
15
+ ### 1. Install dependencies
16
+
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ ### 2. Create your environment file
22
+
23
+ Copy the example file and fill in the required values:
24
+
25
+ ```bash
26
+ cp .env.example .env.local
27
+ ```
28
+
29
+ ### 3. Configure ChatKit credentials
30
+
31
+ Update `.env.local` with the variables that match your setup.
32
+
33
+ - `OPENAI_API_KEY` — API key with access to ChatKit.
34
+ - `NEXT_PUBLIC_CHATKIT_WORKFLOW_ID` — the workflow you created in the ChatKit dashboard.
35
+ - (optional) `CHATKIT_API_BASE` - customizable base URL for the ChatKit API endpoint
36
+
37
+ ### 4. Run the app
38
+
39
+ ```bash
40
+ npm run dev
41
+ ```
42
+
43
+ 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).
44
+
45
+ ### 5. Build for production (optional)
46
+
47
+ ```bash
48
+ npm run build
49
+ npm start
50
+ ```
51
+
52
+ ## Customization Tips
53
+
54
+ - Adjust starter prompts, greeting text, and placeholder copy in [`lib/config.ts`](lib/config.ts).
55
+ - Update the theme defaults or event handlers inside[`components/ChatKitPanel.tsx`](components/ChatKitPanel.tsx) to integrate with your product analytics or storage.
56
+
57
+ ## References
58
+
59
+ - [ChatKit JavaScript Library](http://openai.github.io/chatkit-js/)
60
+ - [Advanced Self-Hosting Examples](https://github.com/openai/openai-chatkit-advanced-samples)
app/App.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-center bg-slate-100 px-6 py-6 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 ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
10
+
11
+ const DEFAULT_CHATKIT_BASE = "https://api.openai.com";
12
+ const SESSION_COOKIE_NAME = "chatkit_session_id";
13
+ const SESSION_COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
14
+
15
+ export async function POST(request: Request): Promise<Response> {
16
+ if (request.method !== "POST") {
17
+ return methodNotAllowedResponse();
18
+ }
19
+ let sessionCookie: string | null = null;
20
+ try {
21
+ const openaiApiKey = process.env.OPENAI_API_KEY;
22
+ if (!openaiApiKey) {
23
+ return new Response(
24
+ JSON.stringify({ error: "Missing OPENAI_API_KEY environment variable" }),
25
+ {
26
+ status: 500,
27
+ headers: { "Content-Type": "application/json" },
28
+ }
29
+ );
30
+ }
31
+
32
+ const parsedBody = await safeParseJson<CreateSessionRequestBody>(request);
33
+ const { userId, sessionCookie: resolvedSessionCookie } = await resolveUserId(request);
34
+ sessionCookie = resolvedSessionCookie;
35
+ const resolvedWorkflowId =
36
+ parsedBody?.workflow?.id ?? parsedBody?.workflowId ?? WORKFLOW_ID;
37
+
38
+ if (process.env.NODE_ENV !== "production") {
39
+ console.info("[create-session] handling request", {
40
+ resolvedWorkflowId,
41
+ body: JSON.stringify(parsedBody),
42
+ });
43
+ }
44
+
45
+ if (!resolvedWorkflowId) {
46
+ return buildJsonResponse(
47
+ { error: "Missing workflow id" },
48
+ 400,
49
+ { "Content-Type": "application/json" },
50
+ sessionCookie
51
+ );
52
+ }
53
+
54
+ const apiBase = process.env.CHATKIT_API_BASE ?? DEFAULT_CHATKIT_BASE;
55
+ const url = `${apiBase}/v1/chatkit/sessions`;
56
+ const upstreamResponse = await fetch(url, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Authorization: `Bearer ${openaiApiKey}`,
61
+ "OpenAI-Beta": "chatkit_beta=v1",
62
+ },
63
+ body: JSON.stringify({
64
+ workflow: { id: resolvedWorkflowId },
65
+ user: userId,
66
+ }),
67
+ });
68
+
69
+ if (process.env.NODE_ENV !== "production") {
70
+ console.info("[create-session] upstream response", {
71
+ status: upstreamResponse.status,
72
+ statusText: upstreamResponse.statusText,
73
+ });
74
+ }
75
+
76
+ const upstreamJson = (await upstreamResponse.json().catch(() => ({}))) as
77
+ | Record<string, unknown>
78
+ | undefined;
79
+
80
+ if (!upstreamResponse.ok) {
81
+ const upstreamError = extractUpstreamError(upstreamJson);
82
+ console.error("OpenAI ChatKit session creation failed", {
83
+ status: upstreamResponse.status,
84
+ statusText: upstreamResponse.statusText,
85
+ body: upstreamJson,
86
+ });
87
+ return buildJsonResponse(
88
+ {
89
+ error: upstreamError ?? `Failed to create session: ${upstreamResponse.statusText}`,
90
+ details: upstreamJson,
91
+ },
92
+ upstreamResponse.status,
93
+ { "Content-Type": "application/json" },
94
+ sessionCookie
95
+ );
96
+ }
97
+
98
+ const clientSecret = upstreamJson?.client_secret ?? null;
99
+ const expiresAfter = upstreamJson?.expires_after ?? null;
100
+ const responsePayload = {
101
+ client_secret: clientSecret,
102
+ expires_after: expiresAfter,
103
+ };
104
+
105
+ return buildJsonResponse(
106
+ responsePayload,
107
+ 200,
108
+ { "Content-Type": "application/json" },
109
+ sessionCookie
110
+ );
111
+ } catch (error) {
112
+ console.error("Create session error", error);
113
+ return buildJsonResponse(
114
+ { error: "Unexpected error" },
115
+ 500,
116
+ { "Content-Type": "application/json" },
117
+ sessionCookie
118
+ );
119
+ }
120
+ }
121
+
122
+ export async function GET(): Promise<Response> {
123
+ return methodNotAllowedResponse();
124
+ }
125
+
126
+ function methodNotAllowedResponse(): Response {
127
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
128
+ status: 405,
129
+ headers: { "Content-Type": "application/json" },
130
+ });
131
+ }
132
+
133
+ async function resolveUserId(request: Request): Promise<{
134
+ userId: string;
135
+ sessionCookie: string | null;
136
+ }> {
137
+ const existing = getCookieValue(request.headers.get("cookie"), SESSION_COOKIE_NAME);
138
+ if (existing) {
139
+ return { userId: existing, sessionCookie: null };
140
+ }
141
+
142
+ const generated = typeof crypto.randomUUID === "function"
143
+ ? crypto.randomUUID()
144
+ : Math.random().toString(36).slice(2);
145
+
146
+ return {
147
+ userId: generated,
148
+ sessionCookie: serializeSessionCookie(generated),
149
+ };
150
+ }
151
+
152
+ function getCookieValue(cookieHeader: string | null, name: string): string | null {
153
+ if (!cookieHeader) {
154
+ return null;
155
+ }
156
+
157
+ const cookies = cookieHeader.split(";");
158
+ for (const cookie of cookies) {
159
+ const [rawName, ...rest] = cookie.split("=");
160
+ if (!rawName || rest.length === 0) { continue; }
161
+ if (rawName.trim() === name) { return rest.join("=").trim(); }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function serializeSessionCookie(value: string): string {
167
+ const attributes = [
168
+ `${SESSION_COOKIE_NAME}=${encodeURIComponent(value)}`,
169
+ "Path=/",
170
+ `Max-Age=${SESSION_COOKIE_MAX_AGE}`,
171
+ "HttpOnly",
172
+ "SameSite=Lax",
173
+ ];
174
+
175
+ if (process.env.NODE_ENV === "production") {
176
+ attributes.push("Secure");
177
+ }
178
+ return attributes.join("; ");
179
+ }
180
+
181
+ function buildJsonResponse(
182
+ payload: unknown,
183
+ status: number,
184
+ headers: Record<string, string>,
185
+ sessionCookie: string | null
186
+ ): Response {
187
+ const responseHeaders = new Headers(headers);
188
+
189
+ if (sessionCookie) {
190
+ responseHeaders.append("Set-Cookie", sessionCookie);
191
+ }
192
+
193
+ return new Response(JSON.stringify(payload), {
194
+ status,
195
+ headers: responseHeaders,
196
+ });
197
+ }
198
+
199
+ async function safeParseJson<T>(req: Request): Promise<T | null> {
200
+ try {
201
+ const text = await req.text();
202
+ if (!text) { return null; }
203
+ return JSON.parse(text) as T;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
209
+ function extractUpstreamError(
210
+ payload: Record<string, unknown> | undefined
211
+ ): string | null {
212
+ if (!payload) {
213
+ return null;
214
+ }
215
+
216
+ const error = payload.error;
217
+ if (typeof error === "string") {
218
+ return error;
219
+ }
220
+
221
+ if (
222
+ error &&
223
+ typeof error === "object" &&
224
+ "message" in error &&
225
+ typeof (error as { message?: unknown }).message === "string"
226
+ ) {
227
+ return (error as { message: string }).message;
228
+ }
229
+
230
+ const details = payload.details;
231
+ if (typeof details === "string") {
232
+ return details;
233
+ }
234
+
235
+ if (details && typeof details === "object" && "error" in details) {
236
+ const nestedError = (details as { error?: unknown }).error;
237
+ if (typeof nestedError === "string") {
238
+ return nestedError;
239
+ }
240
+ if (
241
+ nestedError &&
242
+ typeof nestedError === "object" &&
243
+ "message" in nestedError &&
244
+ typeof (nestedError as { message?: unknown }).message === "string"
245
+ ) {
246
+ return (nestedError as { message: string }).message;
247
+ }
248
+ }
249
+
250
+ if (typeof payload.message === "string") {
251
+ return payload.message;
252
+ }
253
+ return null;
254
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ color-scheme: light;
7
+ }
8
+
9
+ :root[data-color-scheme="dark"] {
10
+ --background: #0a0a0a;
11
+ --foreground: #ededed;
12
+ color-scheme: dark;
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root:not([data-color-scheme]) {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ color-scheme: dark;
20
+ }
21
+ }
22
+
23
+ @theme inline {
24
+ --color-background: var(--background);
25
+ --color-foreground: var(--foreground);
26
+ --font-sans: Arial, Helvetica, sans-serif;
27
+ --font-mono: SFMono-Regular, Consolas, "Liberation Mono", monospace;
28
+ }
29
+
30
+ body {
31
+ background: var(--background);
32
+ color: var(--foreground);
33
+ font-family: var(--font-sans);
34
+ }
app/layout.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import App from "./App";
2
+
3
+ export default function Home() {
4
+ return <App />;
5
+ }
components/ChatKitPanel.tsx ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ } from "@/lib/config";
12
+ import { ErrorOverlay } from "./ErrorOverlay";
13
+ import type { ColorScheme } from "@/hooks/useColorScheme";
14
+
15
+ export type FactAction = {
16
+ type: "save";
17
+ factId: string;
18
+ factText: string;
19
+ };
20
+
21
+ type ChatKitPanelProps = {
22
+ theme: ColorScheme;
23
+ onWidgetAction: (action: FactAction) => Promise<void>;
24
+ onResponseEnd: () => void;
25
+ onThemeRequest: (scheme: ColorScheme) => void;
26
+ };
27
+
28
+ type ErrorState = {
29
+ script: string | null;
30
+ session: string | null;
31
+ integration: string | null;
32
+ retryable: boolean;
33
+ };
34
+
35
+ const isBrowser = typeof window !== "undefined";
36
+ const isDev = process.env.NODE_ENV !== "production";
37
+
38
+ const createInitialErrors = (): ErrorState => ({
39
+ script: null,
40
+ session: null,
41
+ integration: null,
42
+ retryable: false,
43
+ });
44
+
45
+ export function ChatKitPanel({
46
+ theme,
47
+ onWidgetAction,
48
+ onResponseEnd,
49
+ onThemeRequest,
50
+ }: ChatKitPanelProps) {
51
+ const processedFacts = useRef(new Set<string>());
52
+ const [errors, setErrors] = useState<ErrorState>(() => createInitialErrors());
53
+ const [isInitializingSession, setIsInitializingSession] = useState(true);
54
+ const isMountedRef = useRef(true);
55
+ const [scriptStatus, setScriptStatus] = useState<
56
+ "pending" | "ready" | "error"
57
+ >(() =>
58
+ isBrowser && window.customElements?.get("openai-chatkit")
59
+ ? "ready"
60
+ : "pending"
61
+ );
62
+ const [widgetInstanceKey, setWidgetInstanceKey] = useState(0);
63
+
64
+ const setErrorState = useCallback((updates: Partial<ErrorState>) => {
65
+ setErrors((current) => ({ ...current, ...updates }));
66
+ }, []);
67
+
68
+ useEffect(() => {
69
+ return () => {
70
+ isMountedRef.current = false;
71
+ };
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ if (!isBrowser) {
76
+ return;
77
+ }
78
+
79
+ let timeoutId: number | undefined;
80
+
81
+ const handleLoaded = () => {
82
+ if (!isMountedRef.current) {
83
+ return;
84
+ }
85
+ setScriptStatus("ready");
86
+ setErrorState({ script: null });
87
+ };
88
+
89
+ const handleError = (event: Event) => {
90
+ console.error("Failed to load chatkit.js for some reason", event);
91
+ if (!isMountedRef.current) {
92
+ return;
93
+ }
94
+ setScriptStatus("error");
95
+ const detail = (event as CustomEvent<unknown>)?.detail ?? "unknown error";
96
+ setErrorState({ script: `Error: ${detail}`, retryable: false });
97
+ setIsInitializingSession(false);
98
+ };
99
+
100
+ window.addEventListener("chatkit-script-loaded", handleLoaded);
101
+ window.addEventListener(
102
+ "chatkit-script-error",
103
+ handleError as EventListener
104
+ );
105
+
106
+ if (window.customElements?.get("openai-chatkit")) {
107
+ handleLoaded();
108
+ } else if (scriptStatus === "pending") {
109
+ timeoutId = window.setTimeout(() => {
110
+ if (!window.customElements?.get("openai-chatkit")) {
111
+ handleError(
112
+ new CustomEvent("chatkit-script-error", {
113
+ detail:
114
+ "ChatKit web component is unavailable. Verify that the script URL is reachable.",
115
+ })
116
+ );
117
+ }
118
+ }, 5000);
119
+ }
120
+
121
+ return () => {
122
+ window.removeEventListener("chatkit-script-loaded", handleLoaded);
123
+ window.removeEventListener(
124
+ "chatkit-script-error",
125
+ handleError as EventListener
126
+ );
127
+ if (timeoutId) {
128
+ window.clearTimeout(timeoutId);
129
+ }
130
+ };
131
+ }, [scriptStatus, setErrorState]);
132
+
133
+ const isWorkflowConfigured = Boolean(
134
+ WORKFLOW_ID && !WORKFLOW_ID.startsWith("wf_replace")
135
+ );
136
+
137
+ useEffect(() => {
138
+ if (!isWorkflowConfigured && isMountedRef.current) {
139
+ setErrorState({
140
+ session: "Set NEXT_PUBLIC_CHATKIT_WORKFLOW_ID in your .env.local file.",
141
+ retryable: false,
142
+ });
143
+ setIsInitializingSession(false);
144
+ }
145
+ }, [isWorkflowConfigured, setErrorState]);
146
+
147
+ const handleResetChat = useCallback(() => {
148
+ processedFacts.current.clear();
149
+ if (isBrowser) {
150
+ setScriptStatus(
151
+ window.customElements?.get("openai-chatkit") ? "ready" : "pending"
152
+ );
153
+ }
154
+ setIsInitializingSession(true);
155
+ setErrors(createInitialErrors());
156
+ setWidgetInstanceKey((prev) => prev + 1);
157
+ }, []);
158
+
159
+ const getClientSecret = useCallback(
160
+ async (currentSecret: string | null) => {
161
+ if (isDev) {
162
+ console.info("[ChatKitPanel] getClientSecret invoked", {
163
+ currentSecretPresent: Boolean(currentSecret),
164
+ workflowId: WORKFLOW_ID,
165
+ endpoint: CREATE_SESSION_ENDPOINT,
166
+ });
167
+ }
168
+
169
+ if (!isWorkflowConfigured) {
170
+ const detail =
171
+ "Set NEXT_PUBLIC_CHATKIT_WORKFLOW_ID in your .env.local file.";
172
+ if (isMountedRef.current) {
173
+ setErrorState({ session: detail, retryable: false });
174
+ setIsInitializingSession(false);
175
+ }
176
+ throw new Error(detail);
177
+ }
178
+
179
+ if (isMountedRef.current) {
180
+ if (!currentSecret) {
181
+ setIsInitializingSession(true);
182
+ }
183
+ setErrorState({ session: null, integration: null, retryable: false });
184
+ }
185
+
186
+ try {
187
+ const response = await fetch(CREATE_SESSION_ENDPOINT, {
188
+ method: "POST",
189
+ headers: {
190
+ "Content-Type": "application/json",
191
+ },
192
+ body: JSON.stringify({
193
+ workflow: { id: WORKFLOW_ID },
194
+ }),
195
+ });
196
+
197
+ const raw = await response.text();
198
+
199
+ if (isDev) {
200
+ console.info("[ChatKitPanel] createSession response", {
201
+ status: response.status,
202
+ ok: response.ok,
203
+ bodyPreview: raw.slice(0, 1600),
204
+ });
205
+ }
206
+
207
+ let data: Record<string, unknown> = {};
208
+ if (raw) {
209
+ try {
210
+ data = JSON.parse(raw) as Record<string, unknown>;
211
+ } catch (parseError) {
212
+ console.error(
213
+ "Failed to parse create-session response",
214
+ parseError
215
+ );
216
+ }
217
+ }
218
+
219
+ if (!response.ok) {
220
+ const detail = extractErrorDetail(data, response.statusText);
221
+ console.error("Create session request failed", {
222
+ status: response.status,
223
+ body: data,
224
+ });
225
+ throw new Error(detail);
226
+ }
227
+
228
+ const clientSecret = data?.client_secret as string | undefined;
229
+ if (!clientSecret) {
230
+ throw new Error("Missing client secret in response");
231
+ }
232
+
233
+ if (isMountedRef.current) {
234
+ setErrorState({ session: null, integration: null });
235
+ }
236
+
237
+ return clientSecret;
238
+ } catch (error) {
239
+ console.error("Failed to create ChatKit session", error);
240
+ const detail =
241
+ error instanceof Error
242
+ ? error.message
243
+ : "Unable to start ChatKit session.";
244
+ if (isMountedRef.current) {
245
+ setErrorState({ session: detail, retryable: false });
246
+ }
247
+ throw error instanceof Error ? error : new Error(detail);
248
+ } finally {
249
+ if (isMountedRef.current && !currentSecret) {
250
+ setIsInitializingSession(false);
251
+ }
252
+ }
253
+ },
254
+ [isWorkflowConfigured, setErrorState]
255
+ );
256
+
257
+ const chatkit = useChatKit({
258
+ api: { getClientSecret },
259
+ theme: {
260
+ colorScheme: theme,
261
+ color: {
262
+ grayscale: {
263
+ hue: 220,
264
+ tint: 6,
265
+ shade: theme === "dark" ? -1 : -4,
266
+ },
267
+ accent: {
268
+ primary: theme === "dark" ? "#f1f5f9" : "#0f172a",
269
+ level: 1,
270
+ },
271
+ },
272
+ radius: "round",
273
+ },
274
+ startScreen: {
275
+ greeting: GREETING,
276
+ prompts: STARTER_PROMPTS,
277
+ },
278
+ composer: {
279
+ placeholder: PLACEHOLDER_INPUT,
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 flex h-[90vh] w-full 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 ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StartScreenPrompt } from "@openai/chatkit";
2
+
3
+ export const WORKFLOW_ID = process.env.NEXT_PUBLIC_CHATKIT_WORKFLOW_ID?.trim() ?? "";
4
+
5
+ export const CREATE_SESSION_ENDPOINT = "/api/create-session";
6
+
7
+ export const STARTER_PROMPTS: StartScreenPrompt[] = [
8
+ {
9
+ label: "What can you do?",
10
+ prompt: "What can you do?",
11
+ icon: "circle-question",
12
+ },
13
+ ];
14
+
15
+ export const PLACEHOLDER_INPUT = "Ask anything...";
16
+
17
+ export const GREETING = "How can I help you today?";
next.config.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ webpack: (config) => {
5
+ config.resolve.alias = {
6
+ ...(config.resolve.alias ?? {}),
7
+ };
8
+ return config;
9
+ },
10
+ };
11
+
12
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "openai-chatkit-starter-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@openai/chatkit-react": "^0",
13
+ "next": "^15.5.4",
14
+ "react": "^19.2.0",
15
+ "react-dom": "^19.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/eslintrc": "^3",
19
+ "@tailwindcss/postcss": "^4",
20
+ "@types/node": "^20",
21
+ "@types/react": "^19",
22
+ "@types/react-dom": "^19",
23
+ "eslint": "^9",
24
+ "eslint-config-next": "15.5.4",
25
+ "tailwindcss": "^4",
26
+ "typescript": "^5"
27
+ }
28
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }