veryfront 0.1.855 → 0.1.856
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/cli/templates/manifest.js +10 -10
- package/esm/deno.js +1 -1
- package/esm/src/agent/react/use-chat/types.d.ts +2 -2
- package/esm/src/agent/react/use-chat/types.d.ts.map +1 -1
- package/esm/src/agent/react/use-chat/use-chat.js +1 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
|
@@ -11,8 +11,8 @@ export default {
|
|
|
11
11
|
"app/api/workflows/runs/route.ts": "import { listDemoWorkflowRuns } from \"../sample-runs.ts\";\n\nexport function GET(request: Request): Response {\n const url = new URL(request.url);\n const workflowId = url.searchParams.get(\"workflowId\");\n const limit = Number(url.searchParams.get(\"limit\") ?? \"20\");\n\n const runs = listDemoWorkflowRuns({ workflowId, limit });\n\n return Response.json({\n runs,\n totalCount: runs.length,\n });\n}\n",
|
|
12
12
|
"app/api/workflows/sample-runs.ts": "export interface DemoWorkflowStep {\n id: string;\n name: string;\n status: \"pending\" | \"running\" | \"completed\" | \"waiting_for_approval\" | \"failed\";\n output?: string | Record<string, unknown>;\n}\n\nexport interface DemoWorkflowRun {\n id: string;\n workflowId: string;\n status: \"pending\" | \"running\" | \"completed\" | \"waiting_for_approval\" | \"failed\";\n input: { topic: string };\n createdAt: string;\n currentNodes: string[];\n nodeStates: Record<string, { status: DemoWorkflowStep[\"status\"] }>;\n pendingApprovals: Array<{ id: string; status: \"pending\" | \"approved\" | \"rejected\" }>;\n steps: DemoWorkflowStep[];\n}\n\nconst globalStore = globalThis as typeof globalThis & {\n __veryfrontAgenticWorkflowDemoRuns?: Map<string, DemoWorkflowRun>;\n};\nconst demoRuns = globalStore.__veryfrontAgenticWorkflowDemoRuns ??= new Map<\n string,\n DemoWorkflowRun\n>();\n\nexport function createDemoWorkflowRun(\n id = \"test-run\",\n topic = \"Example content pipeline\",\n workflowId = \"content-pipeline\",\n): DemoWorkflowRun {\n return {\n id,\n workflowId,\n status: \"completed\",\n input: { topic },\n createdAt: new Date().toISOString(),\n currentNodes: [],\n nodeStates: {\n research: { status: \"completed\" },\n \"write-article\": { status: \"completed\" },\n \"editorial-review\": { status: \"completed\" },\n publish: { status: \"completed\" },\n },\n pendingApprovals: [],\n steps: [\n {\n id: \"research\",\n name: \"Research\",\n status: \"completed\",\n output: \"Found key points and source material.\",\n },\n {\n id: \"write-article\",\n name: \"Write article\",\n status: \"completed\",\n output: \"Drafted a concise article from the research notes.\",\n },\n {\n id: \"editorial-review\",\n name: \"Editorial review\",\n status: \"completed\",\n },\n {\n id: \"publish\",\n name: \"Publish\",\n status: \"completed\",\n output: { published: true },\n },\n ],\n };\n}\n\nexport function getDemoWorkflowRun(id: string): DemoWorkflowRun {\n return demoRuns.get(id) ?? createDemoWorkflowRun(id);\n}\n\nexport function listDemoWorkflowRuns(options: {\n workflowId?: string | null;\n limit?: number;\n} = {}): DemoWorkflowRun[] {\n if (!demoRuns.has(\"test-run\")) {\n demoRuns.set(\"test-run\", createDemoWorkflowRun());\n }\n\n const limit = Number.isFinite(options.limit) && options.limit && options.limit > 0\n ? options.limit\n : 20;\n\n return Array.from(demoRuns.values())\n .filter((run) => !options.workflowId || run.workflowId === options.workflowId)\n .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))\n .slice(0, limit);\n}\n\nexport function startDemoWorkflowRun(\n workflowId: string,\n input: { topic?: string } = {},\n): DemoWorkflowRun {\n const runId = `run-${Date.now()}`;\n const topic = input.topic?.trim() || \"Untitled workflow\";\n const run = createDemoWorkflowRun(runId, topic, workflowId);\n\n demoRuns.set(run.id, run);\n return run;\n}\n",
|
|
13
13
|
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>AI Workflows</title>\n </Head>\n {children}\n </>\n );\n}\n",
|
|
14
|
-
"app/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { useWorkflowStart, useWorkflowList } from 'veryfront/workflow'\n\nconst STATUS_STYLES: Record<string, string> = {\n running: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',\n completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',\n waiting_for_approval: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',\n failed: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',\n pending: 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400',\n}\n\nexport default function WorkflowDashboard(): JSX.Element {\n const [topic, setTopic] = useState('')\n const { start, isStarting } = useWorkflowStart({ workflowId: 'content-pipeline' })\n const { runs, isLoading, refresh } = useWorkflowList()\n\n async function handleStart(e: React.FormEvent) {\n e.preventDefault()\n if (!topic.trim()) return\n await start({ topic: topic.trim() })\n setTopic('')\n await refresh()\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <div className=\"mb-10\">\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white\">Content Pipeline</h1>\n <p className=\"mt-1 text-neutral-500 dark:text-neutral-400\">Research → Write → Review → Publish</p>\n </div>\n\n {/* Start new workflow */}\n <form onSubmit={handleStart} className=\"mb-10\">\n <div className=\"flex gap-3\">\n <input\n type=\"text\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"Enter a topic to research and write about...\"\n className=\"flex-1 px-4 py-2.5 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500\"\n />\n <button\n type=\"submit\"\n disabled={isStarting || !topic.trim()}\n className=\"px-5 py-2.5 bg-blue-500 text-white font-medium rounded-xl hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isStarting ? 'Starting...' : 'Start'}\n </button>\n </div>\n </form>\n\n {/* Workflow runs */}\n <div>\n <h2 className=\"text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4\">Recent Runs</h2>\n\n {isLoading ? (\n <p className=\"text-neutral-400 text-sm py-8 text-center\">Loading...</p>\n ) : runs.length === 0 ? (\n <div className=\"text-center py-12 bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800\">\n <p className=\"text-neutral-500 dark:text-neutral-400\">No workflows yet. Start one above.</p>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {runs.map((wf) => (\n <a\n key={wf.id}\n href={`/workflows/${wf.id}`}\n className=\"block bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4 hover:border-neutral-300 dark:hover:border-neutral-700 transition-colors\"\n >\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{wf.input?.topic || 'Untitled'}</p>\n <p className=\"text-xs text-neutral-500 mt-1\">{new Date(wf.createdAt).toLocaleString()}</p>\n </div>\n <span className={`px-2.5 py-1 rounded-full text-xs font-medium ${STATUS_STYLES[wf.status] || STATUS_STYLES.pending}`}>\n {wf.status.replace(/_/g, ' ')}\n </span>\n </div>\n </a>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n}\n",
|
|
15
|
-
"app/workflows/[id]/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { usePageContext } from 'veryfront/context'\nimport { useWorkflow } from 'veryfront/workflow'\n\nconst STEP_ICONS: Record<string, string> = {\n completed: '\\u2713',\n running: '\\u25C9',\n pending: '\\u25CB',\n waiting_for_approval: '\\u23F8',\n failed: '\\u2717',\n}\n\nexport default function WorkflowDetail(): JSX.Element {\n const { params } = usePageContext()\n const { run, pendingApprovals, isLoading, refresh } = useWorkflow({ runId: params.id })\n const [isSubmitting, setIsSubmitting] = useState(false)\n\n async function handleApproval(approvalId: string, approved: boolean) {\n setIsSubmitting(true)\n try {\n await fetch(`/api/workflows/runs/${params.id}/approvals/${approvalId}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ approved, approver: 'user' }),\n })\n await refresh()\n } finally {\n setIsSubmitting(false)\n }\n }\n\n if (isLoading) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Loading workflow...</p>\n </div>\n )\n }\n\n if (!run) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Workflow not found</p>\n </div>\n )\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <a href=\"/\" className=\"text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 inline-block\">← Back</a>\n\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white mb-1\">{run.input?.topic || 'Workflow'}</h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mb-8\">Started {new Date(run.createdAt).toLocaleString()}</p>\n\n {/* Steps */}\n <div className=\"space-y-4 mb-8\">\n {run.steps?.map((step: any) => (\n <div key={step.id} className=\"flex items-start gap-3 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4\">\n <span className=\"text-lg mt-0.5\">{STEP_ICONS[step.status] || '\\u25CB'}</span>\n <div className=\"flex-1\">\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{step.name}</p>\n {step.output && (\n <p className=\"text-xs text-neutral-500 mt-1 line-clamp-2\">{typeof step.output === 'string' ? step.output : JSON.stringify(step.output)}</p>\n )}\n </div>\n <span className=\"text-xs text-neutral-400\">{step.status}</span>\n </div>\n ))}\n </div>\n\n {/* Approval */}\n {pendingApprovals.length > 0 && (\n <div className=\"bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-6\">\n <h2 className=\"font-medium text-amber-900 dark:text-amber-200 mb-2\">Approval Required</h2>\n <p className=\"text-sm text-amber-700 dark:text-amber-300 mb-4\">Review the draft before publishing.</p>\n <div className=\"flex gap-3\">\n <button\n onClick={() => handleApproval(pendingApprovals[0].id, true)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-600 disabled:opacity-50 transition-colors text-sm\"\n >\n Approve\n </button>\n <button\n onClick={() => handleApproval(pendingApprovals[0].id, false)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 font-medium rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 disabled:opacity-50 transition-colors text-sm\"\n >\n Reject\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n",
|
|
14
|
+
"app/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { useWorkflowStart, useWorkflowList } from 'veryfront/workflow'\n\nconst STATUS_STYLES: Record<string, string> = {\n running: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',\n completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',\n waiting_for_approval: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',\n failed: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',\n pending: 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400',\n}\n\nexport default function WorkflowDashboard(): React.JSX.Element {\n const [topic, setTopic] = useState('')\n const { start, isStarting } = useWorkflowStart({ workflowId: 'content-pipeline' })\n const { runs, isLoading, refresh } = useWorkflowList()\n\n async function handleStart(e: React.FormEvent) {\n e.preventDefault()\n if (!topic.trim()) return\n await start({ topic: topic.trim() })\n setTopic('')\n await refresh()\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <div className=\"mb-10\">\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white\">Content Pipeline</h1>\n <p className=\"mt-1 text-neutral-500 dark:text-neutral-400\">Research → Write → Review → Publish</p>\n </div>\n\n {/* Start new workflow */}\n <form onSubmit={handleStart} className=\"mb-10\">\n <div className=\"flex gap-3\">\n <input\n type=\"text\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"Enter a topic to research and write about...\"\n className=\"flex-1 px-4 py-2.5 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500\"\n />\n <button\n type=\"submit\"\n disabled={isStarting || !topic.trim()}\n className=\"px-5 py-2.5 bg-blue-500 text-white font-medium rounded-xl hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isStarting ? 'Starting...' : 'Start'}\n </button>\n </div>\n </form>\n\n {/* Workflow runs */}\n <div>\n <h2 className=\"text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4\">Recent Runs</h2>\n\n {isLoading ? (\n <p className=\"text-neutral-400 text-sm py-8 text-center\">Loading...</p>\n ) : runs.length === 0 ? (\n <div className=\"text-center py-12 bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800\">\n <p className=\"text-neutral-500 dark:text-neutral-400\">No workflows yet. Start one above.</p>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {runs.map((wf) => (\n <a\n key={wf.id}\n href={`/workflows/${wf.id}`}\n className=\"block bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4 hover:border-neutral-300 dark:hover:border-neutral-700 transition-colors\"\n >\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{wf.input?.topic || 'Untitled'}</p>\n <p className=\"text-xs text-neutral-500 mt-1\">{new Date(wf.createdAt).toLocaleString()}</p>\n </div>\n <span className={`px-2.5 py-1 rounded-full text-xs font-medium ${STATUS_STYLES[wf.status] || STATUS_STYLES.pending}`}>\n {wf.status.replace(/_/g, ' ')}\n </span>\n </div>\n </a>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n )\n}\n",
|
|
15
|
+
"app/workflows/[id]/page.tsx": "'use client'\n\nimport { useState } from 'react'\nimport { usePageContext } from 'veryfront/context'\nimport { useWorkflow } from 'veryfront/workflow'\n\nconst STEP_ICONS: Record<string, string> = {\n completed: '\\u2713',\n running: '\\u25C9',\n pending: '\\u25CB',\n waiting_for_approval: '\\u23F8',\n failed: '\\u2717',\n}\n\nexport default function WorkflowDetail(): React.JSX.Element {\n const { params } = usePageContext()\n const { run, pendingApprovals, isLoading, refresh } = useWorkflow({ runId: params.id })\n const [isSubmitting, setIsSubmitting] = useState(false)\n\n async function handleApproval(approvalId: string, approved: boolean) {\n setIsSubmitting(true)\n try {\n await fetch(`/api/workflows/runs/${params.id}/approvals/${approvalId}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ approved, approver: 'user' }),\n })\n await refresh()\n } finally {\n setIsSubmitting(false)\n }\n }\n\n if (isLoading) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Loading workflow...</p>\n </div>\n )\n }\n\n if (!run) {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950\">\n <p className=\"text-neutral-400\">Workflow not found</p>\n </div>\n )\n }\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-950\">\n <div className=\"max-w-2xl mx-auto px-4 py-12\">\n <a href=\"/\" className=\"text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 inline-block\">← Back</a>\n\n <h1 className=\"text-2xl font-bold text-neutral-900 dark:text-white mb-1\">{run.input?.topic || 'Workflow'}</h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mb-8\">Started {new Date(run.createdAt).toLocaleString()}</p>\n\n {/* Steps */}\n <div className=\"space-y-4 mb-8\">\n {run.steps?.map((step: any) => (\n <div key={step.id} className=\"flex items-start gap-3 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-4\">\n <span className=\"text-lg mt-0.5\">{STEP_ICONS[step.status] || '\\u25CB'}</span>\n <div className=\"flex-1\">\n <p className=\"font-medium text-neutral-900 dark:text-white text-sm\">{step.name}</p>\n {step.output && (\n <p className=\"text-xs text-neutral-500 mt-1 line-clamp-2\">{typeof step.output === 'string' ? step.output : JSON.stringify(step.output)}</p>\n )}\n </div>\n <span className=\"text-xs text-neutral-400\">{step.status}</span>\n </div>\n ))}\n </div>\n\n {/* Approval */}\n {pendingApprovals.length > 0 && (\n <div className=\"bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-6\">\n <h2 className=\"font-medium text-amber-900 dark:text-amber-200 mb-2\">Approval Required</h2>\n <p className=\"text-sm text-amber-700 dark:text-amber-300 mb-4\">Review the draft before publishing.</p>\n <div className=\"flex gap-3\">\n <button\n onClick={() => handleApproval(pendingApprovals[0].id, true)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-600 disabled:opacity-50 transition-colors text-sm\"\n >\n Approve\n </button>\n <button\n onClick={() => handleApproval(pendingApprovals[0].id, false)}\n disabled={isSubmitting}\n className=\"px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300 font-medium rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 disabled:opacity-50 transition-colors text-sm\"\n >\n Reject\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n",
|
|
16
16
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
17
17
|
"README.md": "# Agentic Workflow\n\nOrchestrated multi-step processes with human approval gates.\n\n## What's included\n\n- Content pipeline workflow (research, write, review, publish)\n- Parallel step execution\n- Human-in-the-loop approval gates\n- Dashboard to start, monitor, and approve workflow runs\n\n## Structure\n\n```\nagents/\n researcher.ts Research agent\n writer.ts Writing agent\nworkflows/content-pipeline.ts Workflow definition\napp/\n api/workflows/ Demo workflow API routes\n page.tsx Workflow dashboard\n workflows/[id]/page.tsx Run detail and approval UI\n```\n\nThis starter is not production-ready.\n",
|
|
18
18
|
"tsconfig.json": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"jsx\": \"react-jsx\",\n \"skipLibCheck\": true,\n \"esModuleInterop\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n",
|
|
@@ -24,7 +24,7 @@ export default {
|
|
|
24
24
|
"agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\n system: \"You are a helpful assistant. Answer questions clearly and concisely.\",\n tools: true,\n maxSteps: 10,\n});\n",
|
|
25
25
|
"app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"assistant\");\n",
|
|
26
26
|
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>AI Chat</title>\n </Head>\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-900\">\n {children}\n </div>\n </>\n );\n}\n",
|
|
27
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function ChatPage(): JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message\" />\n}\n",
|
|
27
|
+
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function ChatPage(): React.JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message\" />\n}\n",
|
|
28
28
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
29
29
|
"README.md": "# AI Agent\n\nA simple conversational AI with tool support.\n\n## What's included\n\n- Single assistant agent with streaming chat UI\n- Example calculator tool\n- `useChat` hook for real-time responses\n\n## Structure\n\n```\nagents/assistant.ts Agent definition\ntools/calculator.ts Example tool\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n",
|
|
30
30
|
"tools/calculator.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"calculator\",\n description: \"Perform basic arithmetic operations\",\n inputSchema: defineSchema((v) => v.object({\n operation: v.enum([\"add\", \"subtract\", \"multiply\", \"divide\"]),\n a: v.number(),\n b: v.number(),\n }))(),\n execute: async ({ operation, a, b }) => {\n if (operation === \"divide\" && b === 0) {\n throw new Error(\"Cannot divide by zero\");\n }\n\n if (operation === \"add\") return { result: a + b };\n if (operation === \"subtract\") return { result: a - b };\n if (operation === \"multiply\") return { result: a * b };\n return { result: a / b };\n },\n});\n",
|
|
@@ -36,7 +36,7 @@ export default {
|
|
|
36
36
|
"agents/coder.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"coder\",\n system: `You are an expert coding assistant. You can read, search, and modify code files in the project.\n\nWhen asked to make changes:\n1. First read the relevant files to understand the codebase\n2. Explain what you'll change and why\n3. Make the changes\n4. Verify the result\n\nAlways explain your reasoning before making edits.`,\n tools: true,\n maxSteps: 15,\n});\n",
|
|
37
37
|
"app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"coder\");\n",
|
|
38
38
|
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>Code Agent</title>\n </Head>\n <div className=\"dark\">\n <div className=\"flex flex-col h-screen bg-neutral-950\">\n <header className=\"flex-shrink-0 border-b border-neutral-800\">\n <div className=\"max-w-4xl mx-auto flex items-center gap-3 px-4 py-3\">\n <div className=\"w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center\">\n <svg\n className=\"w-4 h-4 text-emerald-400\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"\n />\n </svg>\n </div>\n <div>\n <h1 className=\"font-medium text-white text-sm font-mono\">\n code-agent\n </h1>\n <p className=\"text-xs text-neutral-500\">\n read, search, edit project files\n </p>\n </div>\n <div className=\"ml-auto flex items-center gap-1.5\">\n <span className=\"w-2 h-2 rounded-full bg-emerald-400 animate-pulse\" />\n <span className=\"text-xs text-neutral-500 font-mono\">ready</span>\n </div>\n </div>\n </header>\n {children}\n </div>\n </div>\n </>\n );\n}\n",
|
|
39
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function CodeAgent(): JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return (\n <Chat\n {...chat}\n className=\"flex-1 min-h-0\"\n placeholder=\"Describe what you want to build or fix...\"\n />\n )\n}\n",
|
|
39
|
+
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function CodeAgent(): React.JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return (\n <Chat\n {...chat}\n className=\"flex-1 min-h-0\"\n placeholder=\"Describe what you want to build or fix...\"\n />\n )\n}\n",
|
|
40
40
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
41
41
|
"README.md": "# Coding Agent\n\nAn AI assistant that can read, understand, and modify project files.\n\n## What's included\n\n- Coder agent with file system tools\n- Read, list, and edit files through conversation\n- Safe search/replace editing pattern\n\n## Structure\n\n```\nagents/coder.ts Agent with coding instructions\ntools/\n read-file.ts Read file contents\n list-files.ts List directory contents\n edit-file.ts Search and replace in files\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n",
|
|
42
42
|
"tools/edit-file.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\nimport { cwd, readTextFile, realPath, resolve, writeTextFile } from \"veryfront/fs\";\n\n/** True when `target` is the same as, or nested under, `root` (both canonical). */\nfunction isWithin(root: string, target: string): boolean {\n const r = root.replace(/\\\\/g, \"/\");\n const t = target.replace(/\\\\/g, \"/\");\n return t === r || t.startsWith(`${r}/`);\n}\n\nexport default tool({\n id: \"edit-file\",\n description: \"Edit a file by replacing a specific string with new content\",\n inputSchema: defineSchema((v) => v.object({\n path: v.string().describe(\"File path relative to the project root\"),\n search: v.string().describe(\"Exact string to find in the file\"),\n replace: v.string().describe(\"String to replace it with\"),\n }))(),\n execute: async ({ path, search, replace }) => {\n let projectDir: string;\n let absolute: string;\n try {\n // Canonicalize both sides so a symlink that points outside the project\n // is resolved to its real target before the containment check.\n projectDir = await realPath(cwd());\n absolute = await realPath(resolve(projectDir, path));\n } catch {\n return { error: `File not found: ${path}` };\n }\n if (!isWithin(projectDir, absolute)) {\n return { error: `Path escapes project directory: ${path}` };\n }\n\n const content = await readTextFile(absolute);\n if (!content.includes(search)) {\n return { error: \"Search string not found in file\" };\n }\n\n const updated = content.replace(search, replace);\n await writeTextFile(absolute, updated);\n return { path, success: true };\n },\n});\n",
|
|
@@ -66,7 +66,7 @@ export default {
|
|
|
66
66
|
"files": {
|
|
67
67
|
"app/about/page.mdx": "<div className=\"prose dark:prose-invert\">\n\n# About\n\nThis is a minimal starter template.\n\n## Features\n\n- HMR for local development\n- MDX support\n- Tailwind CSS\n- Minimal defaults\n\n## Getting Started\n\n1. Edit pages in the `app` directory\n2. Add components in `components`\n3. Customize with `veryfront.config.ts` when needed\n\nHappy coding!\n\n</div>\n",
|
|
68
68
|
"app/layout.tsx": "export default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <div className=\"min-h-screen bg-white text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100\">\n <main className=\"mx-auto max-w-2xl px-6 py-16\">{children}</main>\n </div>\n );\n}\n",
|
|
69
|
-
"app/page.tsx": "export default function HomePage(): JSX.Element {\n return (\n <div>\n <h1 className=\"mb-4 text-4xl font-bold text-neutral-900 dark:text-white\">\n Welcome to Veryfront\n </h1>\n <p className=\"mb-8 text-neutral-600 dark:text-neutral-400\">\n Edit{\" \"}\n <code className=\"rounded bg-neutral-100 px-1.5 py-0.5 text-sm dark:bg-neutral-800\">\n app/page.tsx\n </code>{\" \"}\n to get started.\n </p>\n <div className=\"flex gap-3\">\n <a\n href=\"/about\"\n className=\"rounded-full bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600\"\n >\n About\n </a>\n <a\n href=\"https://veryfront.com/docs\"\n className=\"rounded-full bg-neutral-100 px-4 py-2 text-sm font-medium text-neutral-900 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700\"\n >\n Documentation\n </a>\n </div>\n </div>\n );\n}\n",
|
|
69
|
+
"app/page.tsx": "export default function HomePage(): React.JSX.Element {\n return (\n <div>\n <h1 className=\"mb-4 text-4xl font-bold text-neutral-900 dark:text-white\">\n Welcome to Veryfront\n </h1>\n <p className=\"mb-8 text-neutral-600 dark:text-neutral-400\">\n Edit{\" \"}\n <code className=\"rounded bg-neutral-100 px-1.5 py-0.5 text-sm dark:bg-neutral-800\">\n app/page.tsx\n </code>{\" \"}\n to get started.\n </p>\n <div className=\"flex gap-3\">\n <a\n href=\"/about\"\n className=\"rounded-full bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600\"\n >\n About\n </a>\n <a\n href=\"https://veryfront.com/docs\"\n className=\"rounded-full bg-neutral-100 px-4 py-2 text-sm font-medium text-neutral-900 transition-colors hover:bg-neutral-200 dark:bg-neutral-800 dark:text-white dark:hover:bg-neutral-700\"\n >\n Documentation\n </a>\n </div>\n </div>\n );\n}\n",
|
|
70
70
|
"README.md": "# Minimal\n\nA barebones starter with just pages and routing, no agents, no tools.\n\n## What's included\n\n- Home page with welcome message\n- About page using MDX\n- Tailwind CSS styling with dark mode\n\n## Structure\n\n```\napp/\n layout.tsx Root layout\n page.tsx Home page\n about/page.mdx Markdown content page\n```\n\nThis starter is not production-ready.\n"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
@@ -77,7 +77,7 @@ export default {
|
|
|
77
77
|
"agents/writer.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"writer\",\n system:\n \"You are a writing specialist. \" +\n \"Take research notes and transform them into clear, engaging prose. \" +\n \"Use a professional but approachable tone.\",\n maxSteps: 3,\n});\n",
|
|
78
78
|
"app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"orchestrator\");\n",
|
|
79
79
|
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>Multi-Agent System</title>\n </Head>\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-950\">\n <header className=\"flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800\">\n <div className=\"max-w-3xl mx-auto flex items-center gap-3 px-4 py-3\">\n <div className=\"flex -space-x-2\">\n <div className=\"w-7 h-7 rounded-full bg-blue-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n O\n </div>\n <div className=\"w-7 h-7 rounded-full bg-amber-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n R\n </div>\n <div className=\"w-7 h-7 rounded-full bg-violet-500 ring-2 ring-white dark:ring-neutral-950 flex items-center justify-center text-[10px] font-bold text-white\">\n W\n </div>\n </div>\n <div>\n <h1 className=\"font-medium text-neutral-900 dark:text-white text-sm\">\n Agent Team\n </h1>\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n Orchestrator, Researcher, Writer\n </p>\n </div>\n </div>\n </header>\n {children}\n </div>\n </>\n );\n}\n",
|
|
80
|
-
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function MultiAgentChat(): JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Give the team a task...\" />\n}\n",
|
|
80
|
+
"app/page.tsx": "'use client'\n\nimport { Chat, useChat } from 'veryfront/chat'\n\nexport default function MultiAgentChat(): React.JSX.Element {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Give the team a task...\" />\n}\n",
|
|
81
81
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
82
82
|
"README.md": "# Multi-Agent System\n\nA team of specialized agents that collaborate on tasks.\n\n## What's included\n\n- Orchestrator that delegates to researcher and writer agents\n- Agent-as-tool composition via `getAgentsAsTools()`\n- Web search tool (placeholder, configure your own API)\n\n## Structure\n\n```\nagents/\n orchestrator.ts Coordinates the team\n researcher.ts Gathers information\n writer.ts Produces polished content\ntools/web-search.ts Placeholder search tool\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Chat interface\n```\n\nThis starter is not production-ready.\n",
|
|
83
83
|
"tools/web-search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"web-search\",\n description: \"Search the web for information on a topic\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: async ({ query: _query }) => {\n // Connect a real search API to use this tool.\n // Popular options: Tavily, SerpAPI, Brave Search\n throw new Error(\n \"No search API configured. \" +\n \"See https://veryfront.com/docs/code/guides/tools for setup instructions.\",\n );\n },\n});\n",
|
|
@@ -88,10 +88,10 @@ export default {
|
|
|
88
88
|
"files": {
|
|
89
89
|
"agents/assistant.ts": "import { agent } from \"veryfront/agent\";\n\nexport default agent({\n id: \"assistant\",\n system: \"You are a helpful AI assistant. Be concise and direct.\",\n tools: true,\n memory: { type: \"conversation\", maxMessages: 50 },\n maxSteps: 10,\n});\n",
|
|
90
90
|
"app/api/ag-ui/route.ts": "import { createAgUiHandler } from \"veryfront/agent\";\n\nexport const POST = createAgUiHandler(\"assistant\");\n",
|
|
91
|
-
"app/dashboard/page.tsx": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Chat, useChat } from \"veryfront/chat\";\n\ninterface Conversation {\n id: string;\n title: string;\n updatedAt: string;\n}\n\nconst INITIAL_CONVERSATIONS: Conversation[] = [\n { id: \"1\", title: \"Getting started\", updatedAt: \"Just now\" },\n];\n\nexport default function Dashboard(): JSX.Element {\n const [conversations] = useState<Conversation[]>(INITIAL_CONVERSATIONS);\n const [activeId, setActiveId] = useState(\"1\");\n const chat = useChat({ api: \"/api/ag-ui\" });\n\n return (\n <div className=\"flex h-screen bg-white dark:bg-neutral-950\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-neutral-200 dark:border-neutral-800 flex flex-col bg-neutral-50 dark:bg-neutral-900\">\n <div className=\"p-4 border-b border-neutral-200 dark:border-neutral-800\">\n <button className=\"w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-300 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors\">\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 4.5v15m7.5-7.5h-15\"\n />\n </svg>\n New chat\n </button>\n </div>\n\n <nav className=\"flex-1 overflow-y-auto p-2 space-y-0.5\">\n {conversations.map((conv) => (\n <button\n key={conv.id}\n onClick={() => setActiveId(conv.id)}\n className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${\n activeId === conv.id\n ? \"bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-white\"\n : \"text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800/50\"\n }`}\n >\n <p className=\"truncate\">{conv.title}</p>\n <p className=\"text-xs text-neutral-400 mt-0.5\">\n {conv.updatedAt}\n </p>\n </button>\n ))}\n </nav>\n\n <div className=\"p-4 border-t border-neutral-200 dark:border-neutral-800\">\n <div className=\"flex items-center gap-2\">\n <div className=\"w-8 h-8 rounded-full bg-neutral-200 dark:bg-neutral-700 flex items-center justify-center text-xs font-medium text-neutral-600 dark:text-neutral-300\">\n U\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-neutral-900 dark:text-white truncate\">\n User\n </p>\n <p className=\"text-xs text-neutral-500 truncate\">\n user@example.com\n </p>\n </div>\n </div>\n </div>\n </aside>\n\n {/* Chat */}\n <main className=\"flex-1 flex flex-col\">\n <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message...\" />\n </main>\n </div>\n );\n}\n",
|
|
91
|
+
"app/dashboard/page.tsx": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Chat, useChat } from \"veryfront/chat\";\n\ninterface Conversation {\n id: string;\n title: string;\n updatedAt: string;\n}\n\nconst INITIAL_CONVERSATIONS: Conversation[] = [\n { id: \"1\", title: \"Getting started\", updatedAt: \"Just now\" },\n];\n\nexport default function Dashboard(): React.JSX.Element {\n const [conversations] = useState<Conversation[]>(INITIAL_CONVERSATIONS);\n const [activeId, setActiveId] = useState(\"1\");\n const chat = useChat({ api: \"/api/ag-ui\" });\n\n return (\n <div className=\"flex h-screen bg-white dark:bg-neutral-950\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-neutral-200 dark:border-neutral-800 flex flex-col bg-neutral-50 dark:bg-neutral-900\">\n <div className=\"p-4 border-b border-neutral-200 dark:border-neutral-800\">\n <button className=\"w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-300 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors\">\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={2}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M12 4.5v15m7.5-7.5h-15\"\n />\n </svg>\n New chat\n </button>\n </div>\n\n <nav className=\"flex-1 overflow-y-auto p-2 space-y-0.5\">\n {conversations.map((conv) => (\n <button\n key={conv.id}\n onClick={() => setActiveId(conv.id)}\n className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${\n activeId === conv.id\n ? \"bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-white\"\n : \"text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800/50\"\n }`}\n >\n <p className=\"truncate\">{conv.title}</p>\n <p className=\"text-xs text-neutral-400 mt-0.5\">\n {conv.updatedAt}\n </p>\n </button>\n ))}\n </nav>\n\n <div className=\"p-4 border-t border-neutral-200 dark:border-neutral-800\">\n <div className=\"flex items-center gap-2\">\n <div className=\"w-8 h-8 rounded-full bg-neutral-200 dark:bg-neutral-700 flex items-center justify-center text-xs font-medium text-neutral-600 dark:text-neutral-300\">\n U\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-neutral-900 dark:text-white truncate\">\n User\n </p>\n <p className=\"text-xs text-neutral-500 truncate\">\n user@example.com\n </p>\n </div>\n </div>\n </div>\n </aside>\n\n {/* Chat */}\n <main className=\"flex-1 flex flex-col\">\n <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message...\" />\n </main>\n </div>\n );\n}\n",
|
|
92
92
|
"app/layout.tsx": "import \"../globals.css\";\nimport { Head } from \"veryfront/head\";\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}): React.ReactNode {\n return (\n <>\n <Head>\n <title>AI SaaS</title>\n </Head>\n <div className=\"antialiased\">\n {children}\n </div>\n </>\n );\n}\n",
|
|
93
|
-
"app/login/page.tsx": "\"use client\";\n\n// Demo sign-in: the buttons below pass straight through to /dashboard so the\n// starter is usable out of the box. To wire up real OAuth, scaffold provider\n// routes at app/api/auth/google/route.ts and app/api/auth/github/route.ts and\n// point the hrefs there. See https://veryfront.com/docs/code/guides/oauth.\nexport default function LoginPage(): JSX.Element {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4\">\n <div className=\"w-full max-w-sm\">\n <div className=\"text-center mb-8\">\n <h1 className=\"text-xl font-bold text-neutral-900 dark:text-white\">\n Welcome back\n </h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mt-1\">\n Sign in to continue\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-6 space-y-3\">\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" viewBox=\"0 0 24 24\">\n <path\n fill=\"#4285F4\"\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.76h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.84 14.09a7.12 7.12 0 010-4.18V7.07H2.18A11.99 11.99 0 001 12c0 1.94.46 3.77 1.18 5.43l3.66-3.34z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n />\n </svg>\n Continue with Google\n </a>\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.51 11.51 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z\" />\n </svg>\n Continue with GitHub\n </a>\n </div>\n\n <p className=\"mt-6 text-center text-xs text-neutral-400\">\n <a\n href=\"/\"\n className=\"hover:text-neutral-600 dark:hover:text-neutral-300\"\n >\n ← Back to home\n </a>\n </p>\n </div>\n </div>\n );\n}\n",
|
|
94
|
-
"app/page.tsx": "export default function LandingPage(): JSX.Element {\n return (\n <div className=\"min-h-screen bg-white dark:bg-neutral-950\">\n {/* Nav */}\n <nav className=\"border-b border-neutral-100 dark:border-neutral-900\">\n <div className=\"max-w-5xl mx-auto flex items-center justify-between px-6 h-14\">\n <span className=\"font-semibold text-neutral-900 dark:text-white\">\n AI SaaS\n </span>\n <div className=\"flex items-center gap-4\">\n <a\n href=\"/login\"\n className=\"text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors\"\n >\n Sign in\n </a>\n <a\n href=\"/login\"\n className=\"text-sm px-4 py-1.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Get started\n </a>\n </div>\n </div>\n </nav>\n\n {/* Hero */}\n <main className=\"max-w-5xl mx-auto px-6\">\n <div className=\"pt-24 pb-16 text-center\">\n <h1 className=\"text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white\">\n Your AI-powered platform\n </h1>\n <p className=\"mt-4 text-lg text-neutral-500 dark:text-neutral-400 max-w-lg mx-auto\">\n Built with Veryfront. Agents, tools, and memory are ready for\n production.\n </p>\n <div className=\"mt-8 flex gap-3 justify-center\">\n <a\n href=\"/login\"\n className=\"px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Start free\n </a>\n <a\n href=\"https://veryfront.com/docs/code/guides\"\n className=\"px-6 py-2.5 border border-neutral-200 dark:border-neutral-800 text-neutral-700 dark:text-neutral-300 rounded-full font-medium hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors\"\n >\n Documentation\n </a>\n </div>\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 py-16 border-t border-neutral-100 dark:border-neutral-900\">\n {[\n {\n title: \"AI Agents\",\n desc: \"Define agents with tools, memory, and streaming. Veryfront auto-discovers them from your project.\",\n },\n {\n title: \"Per-User Memory\",\n desc: \"Each user gets their own conversation history, persisted across sessions.\",\n },\n {\n title: \"Production Ready\",\n desc: \"Use auth, rate limiting, and deployment to ship to production with one command.\",\n },\n ].map(({ title, desc }) => (\n <div key={title}>\n <h3 className=\"font-medium text-neutral-900 dark:text-white\">\n {title}\n </h3>\n <p className=\"mt-1 text-sm text-neutral-500 dark:text-neutral-400\">\n {desc}\n </p>\n </div>\n ))}\n </div>\n </main>\n </div>\n );\n}\n",
|
|
93
|
+
"app/login/page.tsx": "\"use client\";\n\n// Demo sign-in: the buttons below pass straight through to /dashboard so the\n// starter is usable out of the box. To wire up real OAuth, scaffold provider\n// routes at app/api/auth/google/route.ts and app/api/auth/github/route.ts and\n// point the hrefs there. See https://veryfront.com/docs/code/guides/oauth.\nexport default function LoginPage(): React.JSX.Element {\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4\">\n <div className=\"w-full max-w-sm\">\n <div className=\"text-center mb-8\">\n <h1 className=\"text-xl font-bold text-neutral-900 dark:text-white\">\n Welcome back\n </h1>\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400 mt-1\">\n Sign in to continue\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-6 space-y-3\">\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" viewBox=\"0 0 24 24\">\n <path\n fill=\"#4285F4\"\n d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.76h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.84 14.09a7.12 7.12 0 010-4.18V7.07H2.18A11.99 11.99 0 001 12c0 1.94.46 3.77 1.18 5.43l3.66-3.34z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"\n />\n </svg>\n Continue with Google\n </a>\n <a\n href=\"/dashboard\"\n className=\"flex items-center justify-center gap-2 w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors\"\n >\n <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.51 11.51 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z\" />\n </svg>\n Continue with GitHub\n </a>\n </div>\n\n <p className=\"mt-6 text-center text-xs text-neutral-400\">\n <a\n href=\"/\"\n className=\"hover:text-neutral-600 dark:hover:text-neutral-300\"\n >\n ← Back to home\n </a>\n </p>\n </div>\n </div>\n );\n}\n",
|
|
94
|
+
"app/page.tsx": "export default function LandingPage(): React.JSX.Element {\n return (\n <div className=\"min-h-screen bg-white dark:bg-neutral-950\">\n {/* Nav */}\n <nav className=\"border-b border-neutral-100 dark:border-neutral-900\">\n <div className=\"max-w-5xl mx-auto flex items-center justify-between px-6 h-14\">\n <span className=\"font-semibold text-neutral-900 dark:text-white\">\n AI SaaS\n </span>\n <div className=\"flex items-center gap-4\">\n <a\n href=\"/login\"\n className=\"text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors\"\n >\n Sign in\n </a>\n <a\n href=\"/login\"\n className=\"text-sm px-4 py-1.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Get started\n </a>\n </div>\n </div>\n </nav>\n\n {/* Hero */}\n <main className=\"max-w-5xl mx-auto px-6\">\n <div className=\"pt-24 pb-16 text-center\">\n <h1 className=\"text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white\">\n Your AI-powered platform\n </h1>\n <p className=\"mt-4 text-lg text-neutral-500 dark:text-neutral-400 max-w-lg mx-auto\">\n Built with Veryfront. Agents, tools, and memory are ready for\n production.\n </p>\n <div className=\"mt-8 flex gap-3 justify-center\">\n <a\n href=\"/login\"\n className=\"px-6 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-full font-medium hover:opacity-90 transition-opacity\"\n >\n Start free\n </a>\n <a\n href=\"https://veryfront.com/docs/code/guides\"\n className=\"px-6 py-2.5 border border-neutral-200 dark:border-neutral-800 text-neutral-700 dark:text-neutral-300 rounded-full font-medium hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors\"\n >\n Documentation\n </a>\n </div>\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6 py-16 border-t border-neutral-100 dark:border-neutral-900\">\n {[\n {\n title: \"AI Agents\",\n desc: \"Define agents with tools, memory, and streaming. Veryfront auto-discovers them from your project.\",\n },\n {\n title: \"Per-User Memory\",\n desc: \"Each user gets their own conversation history, persisted across sessions.\",\n },\n {\n title: \"Production Ready\",\n desc: \"Use auth, rate limiting, and deployment to ship to production with one command.\",\n },\n ].map(({ title, desc }) => (\n <div key={title}>\n <h3 className=\"font-medium text-neutral-900 dark:text-white\">\n {title}\n </h3>\n <p className=\"mt-1 text-sm text-neutral-500 dark:text-neutral-400\">\n {desc}\n </p>\n </div>\n ))}\n </div>\n </main>\n </div>\n );\n}\n",
|
|
95
95
|
"globals.css": "@import \"tailwindcss\";\n",
|
|
96
96
|
"README.md": "# SaaS Starter\n\nA SaaS-shaped starter with authentication, conversation memory, and a full UI.\n\n## What's included\n\n- Landing page with feature highlights\n- OAuth login (Google and GitHub)\n- Dashboard with conversation sidebar\n- Per-user conversation memory persisted across sessions\n\n## Structure\n\n```\nagents/assistant.ts Agent with conversation memory\ntools/search.ts Placeholder domain search\napp/\n api/ag-ui/route.ts AG-UI endpoint\n page.tsx Landing page\n login/page.tsx OAuth login\n dashboard/page.tsx Chat with sidebar\n```\n\nThis starter is not production-ready.\n",
|
|
97
97
|
"tools/search.ts": "import { tool } from \"veryfront/tool\";\nimport { defineSchema } from \"veryfront/schemas\";\n\nexport default tool({\n id: \"search\",\n description: \"Search your knowledge base\",\n inputSchema: defineSchema((v) => v.object({\n query: v.string().describe(\"Search query\"),\n }))(),\n execute: async ({ query }) => {\n // Replace with your domain-specific search logic\n return {\n results: [],\n query,\n message: \"Connect your data source for real results.\",\n };\n },\n});\n",
|
|
@@ -104,7 +104,7 @@ export default {
|
|
|
104
104
|
"app/api/integrations/token-storage/route.ts": "/**\n * Token Storage Status API\n *\n * Returns the current token storage mode and encryption status.\n * This endpoint is self-contained to work with any version of token-store.\n */\nexport async function GET(): Promise<Response> {\n const env = process.env;\n\n let mode: \"memory\" | \"database\" | \"kv\" | \"redis\" = \"memory\";\n if (env.DATABASE_URL) {\n mode = \"database\";\n } else if (env.KV_REST_API_URL) {\n mode = \"kv\";\n } else if (env.REDIS_URL) {\n mode = \"redis\";\n }\n\n const hasExplicitKey = env.TOKEN_ENCRYPTION_KEY?.length === 64;\n\n return Response.json({\n mode,\n encrypted: true,\n autoGenerated: !hasExplicitKey,\n });\n}\n",
|
|
105
105
|
"app/components/ServiceConnections.tsx": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ninterface Service {\n id: string;\n name: string;\n connected: boolean;\n authUrl: string;\n}\n\ninterface ServiceConnectionsProps {\n services: Array<{\n id: string;\n name: string;\n authUrl: string;\n }>;\n className?: string;\n}\n\nfunction useIntegrationStatus(): { status: Record<string, boolean>; loading: boolean } {\n const [status, setStatus] = useState<Record<string, boolean>>({});\n const [loading, setLoading] = useState<boolean>(true);\n\n useEffect(() => {\n async function checkStatus(): Promise<void> {\n try {\n const res = await fetch(\"/api/integrations/status\");\n if (!res.ok) return;\n\n const data = await res.json();\n const integrations = data?.integrations ?? [];\n\n const statusMap: Record<string, boolean> = {};\n for (const integration of integrations) {\n statusMap[integration.id] = integration.connected;\n }\n\n setStatus(statusMap);\n } catch (error) {\n console.error(\"Failed to check service status:\", error);\n } finally {\n setLoading(false);\n }\n }\n\n checkStatus();\n }, []);\n\n return { status, loading };\n}\n\nfunction withStatus(\n services: ServiceConnectionsProps[\"services\"],\n status: Record<string, boolean>,\n): Service[] {\n return services.map((service) => ({\n ...service,\n connected: status[service.id] ?? false,\n }));\n}\n\nexport function ServiceConnections({\n services,\n className = \"\",\n}: ServiceConnectionsProps): React.ReactElement {\n const { status, loading } = useIntegrationStatus();\n\n if (loading) {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n <div className=\"animate-pulse h-6 w-32 bg-neutral-200 dark:bg-neutral-700 rounded\" />\n </div>\n );\n }\n\n const servicesWithStatus = withStatus(services, status);\n const connectedCount = servicesWithStatus.reduce(\n (count, service) => count + (service.connected ? 1 : 0),\n 0,\n );\n\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n {servicesWithStatus.map((service) => (\n <ServiceBadge key={service.id} service={service} />\n ))}\n {connectedCount < services.length && (\n <span className=\"text-xs text-neutral-500 dark:text-neutral-400 ml-1\">\n {connectedCount}/{services.length} connected\n </span>\n )}\n </div>\n );\n}\n\nfunction ServiceBadge({ service }: { service: Service }): React.ReactElement {\n if (service.connected) {\n return (\n <span\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400\"\n title={`${service.name} connected`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-green-500\" />\n {service.name}\n </span>\n );\n }\n\n const handleConnect = (): void => {\n globalThis.location.href = service.authUrl;\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleConnect}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700 transition-colors\"\n title={`Connect ${service.name}`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-neutral-400\" />\n {service.name}\n </button>\n );\n}\n\nexport function ServiceConnectionsCard({\n services,\n className = \"\",\n}: ServiceConnectionsProps): React.ReactElement | null {\n const { status, loading } = useIntegrationStatus();\n\n if (loading) return null;\n\n const disconnectedServices = withStatus(services, status).filter((service) => !service.connected);\n if (disconnectedServices.length === 0) return null;\n\n return (\n <div\n className={`rounded-lg border border-amber-200 dark:border-amber-900/50 bg-amber-50 dark:bg-amber-900/20 p-4 ${className}`}\n >\n <h3 className=\"font-medium text-amber-900 dark:text-amber-200 mb-2\">\n Connect your services\n </h3>\n <p className=\"text-sm text-amber-700 dark:text-amber-300/80 mb-3\">\n Connect the following services to unlock all features:\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {disconnectedServices.map((service) => (\n <a\n key={service.id}\n href={service.authUrl}\n className=\"inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium bg-amber-100 text-amber-800 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60 transition-colors\"\n >\n Connect {service.name}\n </a>\n ))}\n </div>\n </div>\n );\n}\n",
|
|
106
106
|
"app/page.tsx": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Chat, useChat } from 'veryfront/chat'\n\ninterface Integration {\n id: string\n name: string\n connected: boolean\n connectUrl: string\n}\n\nexport default function ChatPage(): React.ReactElement {\n const chat = useChat({ api: '/api/ag-ui' })\n\n return (\n <div className=\"flex flex-col h-screen bg-white dark:bg-neutral-900\">\n <header className=\"sticky top-0 z-10 flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900\">\n <div className=\"px-4 py-3 flex items-center justify-between\">\n <h1 className=\"font-medium text-neutral-900 dark:text-white\">AI Agent</h1>\n <div className=\"flex items-center gap-4\">\n <ServiceStatusFromAPI />\n <a\n href=\"/setup\"\n className=\"text-sm text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200\"\n >\n Setup\n </a>\n </div>\n </div>\n </header>\n\n <Chat {...chat} className=\"flex-1 min-h-0\" placeholder=\"Message\" />\n </div>\n )\n}\n\nfunction ServiceStatusFromAPI(): React.ReactElement | null {\n const [integrations, setIntegrations] = useState<Integration[]>([])\n const [loading, setLoading] = useState<boolean>(true)\n\n useEffect((): void => {\n async function fetchStatus(): Promise<void> {\n try {\n const res = await fetch('/api/integrations/status')\n if (!res.ok) return\n\n const data = await res.json()\n setIntegrations(data.integrations ?? [])\n } catch (error) {\n console.error('Failed to fetch integration status:', error)\n } finally {\n setLoading(false)\n }\n }\n\n void fetchStatus()\n }, [])\n\n if (loading) {\n return (\n <div className=\"flex items-center gap-2\">\n <div className=\"animate-pulse h-6 w-24 bg-neutral-200 dark:bg-neutral-700 rounded-full\" />\n </div>\n )\n }\n\n if (integrations.length === 0) return null\n\n const connected: Integration[] = []\n const disconnected: Integration[] = []\n\n for (const integration of integrations) {\n if (integration.connected) connected.push(integration)\n else disconnected.push(integration)\n }\n\n return (\n <div className=\"flex items-center gap-2\">\n {connected.map(service => (\n <span\n key={service.id}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400\"\n title={`${service.name} connected`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-green-500\" />\n {service.name}\n </span>\n ))}\n\n {disconnected.map(service => (\n <a\n key={service.id}\n href={service.connectUrl}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700 transition-colors\"\n title={`Connect ${service.name}`}\n >\n <span className=\"w-1.5 h-1.5 rounded-full bg-neutral-400\" />\n {service.name}\n </a>\n ))}\n\n {disconnected.length > 0 && (\n <span className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {connected.length}/{integrations.length}\n </span>\n )}\n </div>\n )\n}\n",
|
|
107
|
-
"app/setup/page-helpers.tsx": "import type { JSX } from \"react\";\n\nexport interface Integration {\n id: string;\n name: string;\n icon: string;\n connected: boolean;\n connectUrl: string;\n}\n\nexport interface SetupStep {\n id: string;\n title: string;\n description: string;\n completed: boolean;\n action?: () => void;\n link?: string;\n}\n\ninterface SetupGuide {\n title: string;\n steps: string[];\n link: string;\n envVars: string[];\n category: string;\n}\n\nexport interface TokenStorageStatus {\n mode: \"memory\" | \"database\" | \"kv\" | \"redis\" | \"custom\";\n encrypted: boolean;\n autoGenerated?: boolean;\n}\n\nexport type TokenStorageStyles = {\n container: string;\n iconWrapper: string;\n title: string;\n text: string;\n isMemory: boolean;\n};\n\nexport const CATEGORIES = [\n { id: \"google\", name: \"Google Services\", icon: \"google\" },\n { id: \"microsoft\", name: \"Microsoft Services\", icon: \"microsoft\" },\n { id: \"atlassian\", name: \"Atlassian\", icon: \"atlassian\" },\n { id: \"communication\", name: \"Communication\", icon: \"chat\" },\n { id: \"development\", name: \"Development\", icon: \"code\" },\n { id: \"productivity\", name: \"Productivity\", icon: \"tasks\" },\n { id: \"storage\", name: \"Storage\", icon: \"folder\" },\n { id: \"infrastructure\", name: \"Infrastructure\", icon: \"server\" },\n { id: \"sales\", name: \"Sales & CRM\", icon: \"users\" },\n { id: \"support\", name: \"Support\", icon: \"headset\" },\n { id: \"finance\", name: \"Finance\", icon: \"dollar\" },\n { id: \"marketing\", name: \"Marketing\", icon: \"megaphone\" },\n { id: \"design\", name: \"Design\", icon: \"palette\" },\n { id: \"ai\", name: \"AI Providers\", icon: \"brain\" },\n] as const;\n\nexport const OAUTH_SETUP_GUIDES: Record<string, SetupGuide> = {\n gmail: {\n title: \"Google OAuth Setup (Gmail)\",\n category: \"google\",\n steps: [\n \"Go to Google Cloud Console\",\n \"Create a new project or select existing one\",\n \"Enable Gmail API in APIs & Services > Library\",\n \"Go to APIs & Services > Credentials\",\n \"Create OAuth 2.0 credentials (Web application)\",\n \"Add redirect URI: http://localhost:3000/api/auth/gmail/callback\",\n \"Copy Client ID and Secret to your .env file\",\n ],\n link: \"https://console.cloud.google.com/apis/credentials\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n calendar: {\n title: \"Google Calendar Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials as Gmail\",\n \"Enable Calendar API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/calendar/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/calendar-json.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n drive: {\n title: \"Google Drive Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Drive API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/drive/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/drive.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n sheets: {\n title: \"Google Sheets Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Sheets API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/sheets/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/sheets.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n \"docs-google\": {\n title: \"Google Docs Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Docs API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/docs-google/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/docs.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n outlook: {\n title: \"Microsoft Outlook Setup\",\n category: \"microsoft\",\n steps: [\n \"Go to Azure Portal > Azure Active Directory\",\n \"Click App registrations > New registration\",\n \"Set redirect URI: http://localhost:3000/api/auth/outlook/callback\",\n \"Go to API permissions > Add Microsoft Graph permissions\",\n \"Add: Mail.Read, Mail.Send, Mail.ReadWrite\",\n \"Go to Certificates & secrets > New client secret\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n teams: {\n title: \"Microsoft Teams Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials as Outlook\",\n \"Add Teams permissions: Chat.Read, Chat.ReadWrite, Channel.ReadBasic.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/teams/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n onedrive: {\n title: \"Microsoft OneDrive Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Files.Read, Files.ReadWrite\",\n \"Add redirect URI: http://localhost:3000/api/auth/onedrive/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n sharepoint: {\n title: \"Microsoft SharePoint Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Sites.Read.All, Sites.ReadWrite.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/sharepoint/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n jira: {\n title: \"Atlassian Jira Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Atlassian Developer Console\",\n \"Click Create > OAuth 2.0 integration\",\n \"Add Jira API scopes: read:jira-work, write:jira-work\",\n \"Set callback URL: http://localhost:3000/api/auth/jira/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n confluence: {\n title: \"Atlassian Confluence Setup\",\n category: \"atlassian\",\n steps: [\n \"Uses same Atlassian OAuth credentials as Jira\",\n \"Add Confluence scopes: read:confluence-content.all, write:confluence-content\",\n \"Add callback URL: http://localhost:3000/api/auth/confluence/callback\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n bitbucket: {\n title: \"Atlassian Bitbucket Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Bitbucket Settings > OAuth consumers\",\n \"Click Add consumer\",\n \"Set callback URL: http://localhost:3000/api/auth/bitbucket/callback\",\n \"Add permissions: repository:read, repository:write\",\n \"Copy Key and Secret to .env\",\n ],\n link: \"https://bitbucket.org/account/settings/app-passwords/\",\n envVars: [\"BITBUCKET_CLIENT_ID\", \"BITBUCKET_CLIENT_SECRET\"],\n },\n slack: {\n title: \"Slack App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Slack API Apps page\",\n \"Click Create New App > From scratch\",\n \"Go to OAuth & Permissions\",\n \"Add scopes: channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, users:read\",\n \"Add redirect URL: http://localhost:3000/api/auth/slack/callback\",\n \"Install to Workspace\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://api.slack.com/apps\",\n envVars: [\"SLACK_CLIENT_ID\", \"SLACK_CLIENT_SECRET\"],\n },\n zoom: {\n title: \"Zoom App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Zoom App Marketplace\",\n \"Click Develop > Build App\",\n \"Choose OAuth app type\",\n \"Add redirect URL: http://localhost:3000/api/auth/zoom/callback\",\n \"Add scopes: meeting:read, meeting:write, user:read\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://marketplace.zoom.us/develop/create\",\n envVars: [\"ZOOM_CLIENT_ID\", \"ZOOM_CLIENT_SECRET\"],\n },\n webex: {\n title: \"Webex Integration Setup\",\n category: \"communication\",\n steps: [\n \"Go to Webex Developer Portal\",\n \"Create a new integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/webex/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.webex.com/my-apps\",\n envVars: [\"WEBEX_CLIENT_ID\", \"WEBEX_CLIENT_SECRET\"],\n },\n twilio: {\n title: \"Twilio Setup\",\n category: \"communication\",\n steps: [\n \"Go to Twilio Console\",\n \"Copy Account SID and Auth Token\",\n \"Get a phone number for SMS\",\n \"Add credentials to .env\",\n ],\n link: \"https://console.twilio.com/\",\n envVars: [\"TWILIO_ACCOUNT_SID\", \"TWILIO_AUTH_TOKEN\", \"TWILIO_PHONE_NUMBER\"],\n },\n github: {\n title: \"GitHub OAuth App Setup\",\n category: \"development\",\n steps: [\n \"Go to GitHub Developer Settings\",\n \"Click OAuth Apps > New OAuth App\",\n \"Set Homepage URL: http://localhost:3000\",\n \"Set callback URL: http://localhost:3000/api/auth/github/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://github.com/settings/developers\",\n envVars: [\"GITHUB_CLIENT_ID\", \"GITHUB_CLIENT_SECRET\"],\n },\n gitlab: {\n title: \"GitLab OAuth Setup\",\n category: \"development\",\n steps: [\n \"Go to GitLab User Settings > Applications\",\n \"Create new application\",\n \"Add redirect URI: http://localhost:3000/api/auth/gitlab/callback\",\n \"Select scopes: api, read_user, read_repository\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://gitlab.com/-/profile/applications\",\n envVars: [\"GITLAB_CLIENT_ID\", \"GITLAB_CLIENT_SECRET\"],\n },\n sentry: {\n title: \"Sentry Setup\",\n category: \"development\",\n steps: [\n \"Go to Sentry Settings > Developer Settings\",\n \"Create new integration\",\n \"Add redirect URL: http://localhost:3000/api/auth/sentry/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://sentry.io/settings/developer-settings/\",\n envVars: [\"SENTRY_CLIENT_ID\", \"SENTRY_CLIENT_SECRET\"],\n },\n posthog: {\n title: \"PostHog Setup\",\n category: \"development\",\n steps: [\"Go to PostHog Project Settings\", \"Copy your Project API Key\", \"Add to .env file\"],\n link: \"https://app.posthog.com/project/settings\",\n envVars: [\"POSTHOG_API_KEY\", \"POSTHOG_HOST\"],\n },\n mixpanel: {\n title: \"Mixpanel Setup\",\n category: \"development\",\n steps: [\n \"Go to Mixpanel Project Settings\",\n \"Copy your Project Token\",\n \"For API access, create a Service Account\",\n \"Add credentials to .env\",\n ],\n link: \"https://mixpanel.com/settings/project\",\n envVars: [\"MIXPANEL_TOKEN\", \"MIXPANEL_API_SECRET\"],\n },\n notion: {\n title: \"Notion Integration Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Notion Integrations page\",\n \"Click New integration\",\n \"Name your integration and select workspace\",\n \"Copy the Internal Integration Token\",\n \"Share desired pages/databases with your integration\",\n \"Add token to .env\",\n ],\n link: \"https://www.notion.so/my-integrations\",\n envVars: [\"NOTION_API_KEY\"],\n },\n linear: {\n title: \"Linear OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Linear Settings > API\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/linear/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://linear.app/settings/api\",\n envVars: [\"LINEAR_CLIENT_ID\", \"LINEAR_CLIENT_SECRET\"],\n },\n asana: {\n title: \"Asana OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Asana Developer Console\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/asana/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.asana.com/0/developer-console\",\n envVars: [\"ASANA_CLIENT_ID\", \"ASANA_CLIENT_SECRET\"],\n },\n trello: {\n title: \"Trello Power-Up Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Trello Power-Ups Admin\",\n \"Create new Power-Up\",\n \"Add redirect URI: http://localhost:3000/api/auth/trello/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://trello.com/power-ups/admin\",\n envVars: [\"TRELLO_API_KEY\", \"TRELLO_API_SECRET\"],\n },\n monday: {\n title: \"Monday.com App Setup\",\n category: \"productivity\",\n steps: [\n \"Go to monday.com Developers\",\n \"Create new app\",\n \"Add OAuth redirect: http://localhost:3000/api/auth/monday/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://monday.com/developers/apps\",\n envVars: [\"MONDAY_CLIENT_ID\", \"MONDAY_CLIENT_SECRET\"],\n },\n clickup: {\n title: \"ClickUp OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to ClickUp Settings > Apps\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/clickup/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.clickup.com/settings/apps\",\n envVars: [\"CLICKUP_CLIENT_ID\", \"CLICKUP_CLIENT_SECRET\"],\n },\n box: {\n title: \"Box App Setup\",\n category: \"storage\",\n steps: [\n \"Go to Box Developer Console\",\n \"Create new app with OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/box/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.box.com/developers/console\",\n envVars: [\"BOX_CLIENT_ID\", \"BOX_CLIENT_SECRET\"],\n },\n airtable: {\n title: \"Airtable OAuth Setup\",\n category: \"storage\",\n steps: [\n \"Go to Airtable Developer Hub\",\n \"Create new OAuth integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/airtable/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://airtable.com/create/oauth\",\n envVars: [\"AIRTABLE_CLIENT_ID\", \"AIRTABLE_CLIENT_SECRET\"],\n },\n supabase: {\n title: \"Supabase Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Supabase Dashboard\",\n \"Create new project or select existing\",\n \"Go to Settings > API\",\n \"Copy Project URL and anon/service_role keys\",\n \"Add to .env file\",\n ],\n link: \"https://supabase.com/dashboard\",\n envVars: [\"SUPABASE_URL\", \"SUPABASE_ANON_KEY\", \"SUPABASE_SERVICE_ROLE_KEY\"],\n },\n neon: {\n title: \"Neon Database Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Neon Console\",\n \"Create new project\",\n \"Copy connection string from Dashboard\",\n \"Add to .env file\",\n ],\n link: \"https://console.neon.tech/\",\n envVars: [\"DATABASE_URL\"],\n },\n snowflake: {\n title: \"Snowflake Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Snowflake Console\",\n \"Create a service account or use existing credentials\",\n \"Note your account identifier, warehouse, database\",\n \"Add credentials to .env\",\n ],\n link: \"https://app.snowflake.com/\",\n envVars: [\"SNOWFLAKE_ACCOUNT\", \"SNOWFLAKE_USER\", \"SNOWFLAKE_PASSWORD\", \"SNOWFLAKE_WAREHOUSE\"],\n },\n aws: {\n title: \"AWS Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to AWS IAM Console\",\n \"Create new IAM user with programmatic access\",\n \"Attach required policies (S3, Lambda, DynamoDB)\",\n \"Copy Access Key ID and Secret\",\n \"Add to .env file\",\n ],\n link: \"https://console.aws.amazon.com/iam/\",\n envVars: [\"AWS_ACCESS_KEY_ID\", \"AWS_SECRET_ACCESS_KEY\", \"AWS_REGION\"],\n },\n salesforce: {\n title: \"Salesforce Connected App Setup\",\n category: \"sales\",\n steps: [\n \"Go to Salesforce Setup > App Manager\",\n \"Create new Connected App\",\n \"Enable OAuth Settings\",\n \"Add callback URL: http://localhost:3000/api/auth/salesforce/callback\",\n \"Select OAuth scopes: api, refresh_token\",\n \"Copy Consumer Key and Secret to .env\",\n ],\n link: \"https://login.salesforce.com/\",\n envVars: [\"SALESFORCE_CLIENT_ID\", \"SALESFORCE_CLIENT_SECRET\"],\n },\n pipedrive: {\n title: \"Pipedrive OAuth Setup\",\n category: \"sales\",\n steps: [\n \"Go to Pipedrive Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/pipedrive/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.pipedrive.com/\",\n envVars: [\"PIPEDRIVE_CLIENT_ID\", \"PIPEDRIVE_CLIENT_SECRET\"],\n },\n zendesk: {\n title: \"Zendesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Zendesk Admin > API > OAuth Clients\",\n \"Add new OAuth client\",\n \"Set redirect URI: http://localhost:3000/api/auth/zendesk/callback\",\n \"Copy Client ID and Secret to .env\",\n \"Add your Zendesk subdomain\",\n ],\n link: \"https://support.zendesk.com/hc/en-us/articles/4408845965210\",\n envVars: [\"ZENDESK_CLIENT_ID\", \"ZENDESK_CLIENT_SECRET\", \"ZENDESK_SUBDOMAIN\"],\n },\n intercom: {\n title: \"Intercom OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Intercom Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/intercom/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.intercom.com/\",\n envVars: [\"INTERCOM_CLIENT_ID\", \"INTERCOM_CLIENT_SECRET\"],\n },\n freshdesk: {\n title: \"Freshdesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Freshdesk Admin > Apps > Custom Apps\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/freshdesk/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.freshdesk.com/\",\n envVars: [\"FRESHDESK_CLIENT_ID\", \"FRESHDESK_CLIENT_SECRET\", \"FRESHDESK_DOMAIN\"],\n },\n servicenow: {\n title: \"ServiceNow OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to ServiceNow System OAuth > Application Registry\",\n \"Create OAuth API endpoint for external clients\",\n \"Add redirect URL: http://localhost:3000/api/auth/servicenow/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://docs.servicenow.com/\",\n envVars: [\"SERVICENOW_CLIENT_ID\", \"SERVICENOW_CLIENT_SECRET\", \"SERVICENOW_INSTANCE\"],\n },\n stripe: {\n title: \"Stripe Setup\",\n category: \"finance\",\n steps: [\n \"Go to Stripe Dashboard\",\n \"Go to Developers > API keys\",\n \"Copy Publishable and Secret keys\",\n \"For Connect, set up OAuth in Connect settings\",\n \"Add to .env file\",\n ],\n link: \"https://dashboard.stripe.com/apikeys\",\n envVars: [\"STRIPE_SECRET_KEY\", \"STRIPE_PUBLISHABLE_KEY\"],\n },\n quickbooks: {\n title: \"QuickBooks OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Intuit Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/quickbooks/callback\",\n \"Select Accounting scope\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.intuit.com/app/developer/dashboard\",\n envVars: [\"QUICKBOOKS_CLIENT_ID\", \"QUICKBOOKS_CLIENT_SECRET\"],\n },\n xero: {\n title: \"Xero OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Xero Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/xero/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.xero.com/app/manage\",\n envVars: [\"XERO_CLIENT_ID\", \"XERO_CLIENT_SECRET\"],\n },\n mailchimp: {\n title: \"Mailchimp OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Mailchimp Developer Portal\",\n \"Register new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/mailchimp/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://admin.mailchimp.com/account/oauth2/\",\n envVars: [\"MAILCHIMP_CLIENT_ID\", \"MAILCHIMP_CLIENT_SECRET\"],\n },\n shopify: {\n title: \"Shopify App Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Shopify Partners Dashboard\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/shopify/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://partners.shopify.com/\",\n envVars: [\"SHOPIFY_API_KEY\", \"SHOPIFY_API_SECRET\"],\n },\n twitter: {\n title: \"Twitter/X OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Twitter Developer Portal\",\n \"Create new project and app\",\n \"Enable OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/twitter/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.twitter.com/en/portal/dashboard\",\n envVars: [\"TWITTER_CLIENT_ID\", \"TWITTER_CLIENT_SECRET\"],\n },\n figma: {\n title: \"Figma OAuth Setup\",\n category: \"design\",\n steps: [\n \"Go to Figma Developer Settings\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/figma/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://www.figma.com/developers/apps\",\n envVars: [\"FIGMA_CLIENT_ID\", \"FIGMA_CLIENT_SECRET\"],\n },\n anthropic: {\n title: \"Anthropic API Setup\",\n category: \"ai\",\n steps: [\"Go to Anthropic Console\", \"Create new API key\", \"Copy API key to .env\"],\n link: \"https://console.anthropic.com/\",\n envVars: [\"ANTHROPIC_API_KEY\"],\n },\n};\n\nexport function filterIntegrations(\n integrations: Integration[],\n searchQuery: string,\n selectedCategory: string | null,\n): Integration[] {\n const query = searchQuery.toLowerCase();\n\n return integrations.filter((integration) => {\n const guide = OAUTH_SETUP_GUIDES[integration.id];\n\n const matchesSearch =\n query === \"\" ||\n integration.name.toLowerCase().includes(query) ||\n integration.id.toLowerCase().includes(query);\n\n const matchesCategory = selectedCategory === null || guide?.category === selectedCategory;\n\n return matchesSearch && matchesCategory;\n });\n}\n\nexport function groupIntegrationsByCategory(\n integrations: Integration[],\n): Record<string, Integration[]> {\n const groups: Record<string, Integration[]> = {};\n\n for (const integration of integrations) {\n const category = OAUTH_SETUP_GUIDES[integration.id]?.category ?? \"other\";\n (groups[category] ??= []).push(integration);\n }\n\n return groups;\n}\n\nexport function buildSetupSteps(\n envChecked: boolean,\n allConnected: boolean,\n markEnvChecked: () => void,\n): SetupStep[] {\n return [\n {\n id: \"env\",\n title: \"Configure Environment Variables\",\n description: \"Add your OAuth credentials to the .env file\",\n completed: envChecked,\n action: markEnvChecked,\n },\n {\n id: \"oauth\",\n title: \"Create OAuth Apps\",\n description: \"Set up OAuth applications for each service\",\n completed: false,\n },\n {\n id: \"connect\",\n title: \"Connect Services\",\n description: \"Authorize your app to access each service\",\n completed: allConnected,\n },\n ];\n}\n\nexport function getTokenStorageStyles(\n tokenStorage: TokenStorageStatus | null,\n): TokenStorageStyles | null {\n if (!tokenStorage) return null;\n\n const isMemory = tokenStorage.mode === \"memory\";\n\n return {\n container: `rounded-2xl p-6 shadow-sm border mb-8 ${\n isMemory\n ? \"bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800\"\n : \"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800\"\n }`,\n iconWrapper: `w-10 h-10 rounded-full flex items-center justify-center ${\n isMemory ? \"bg-amber-100 dark:bg-amber-900\" : \"bg-green-100 dark:bg-green-900\"\n }`,\n title: `font-semibold ${\n isMemory ? \"text-amber-800 dark:text-amber-200\" : \"text-green-800 dark:text-green-200\"\n }`,\n text: `text-sm mt-1 ${\n isMemory ? \"text-amber-700 dark:text-amber-300\" : \"text-green-700 dark:text-green-300\"\n }`,\n isMemory,\n };\n}\n\nexport function ServiceIcon({ name }: { name: string }): JSX.Element {\n const iconMap: Record<string, JSX.Element> = {\n mail: (\n <svg className=\"w-6 h-6 text-red-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n fill=\"none\"\n />\n </svg>\n ),\n slack: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\"\n fill=\"#E01E5A\"\n />\n <path\n d=\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\"\n fill=\"#36C5F0\"\n />\n <path\n d=\"M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z\"\n fill=\"#2EB67D\"\n />\n <path\n d=\"M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\"\n fill=\"#ECB22E\"\n />\n </svg>\n ),\n calendar: (\n <svg\n className=\"w-6 h-6 text-blue-500\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"\n />\n </svg>\n ),\n github: (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n fillRule=\"evenodd\"\n d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\"\n clipRule=\"evenodd\"\n />\n </svg>\n ),\n jira: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\">\n <defs>\n <linearGradient id=\"jira-gradient\" x1=\"98.031%\" x2=\"58.888%\" y1=\".161%\" y2=\"40.766%\">\n <stop offset=\"0%\" stopColor=\"#0052CC\" />\n <stop offset=\"100%\" stopColor=\"#2684FF\" />\n </linearGradient>\n </defs>\n <path\n fill=\"url(#jira-gradient)\"\n d=\"M11.571 11.513H0a5.218 5.218 0 005.232 5.215h2.13v2.057A5.215 5.215 0 0012.575 24V12.518a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M17.151 5.97H5.58a5.215 5.215 0 005.215 5.214h2.129v2.058a5.218 5.218 0 005.232 5.215V6.975a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M22.723.426H11.152a5.215 5.215 0 005.215 5.215h2.129v2.057a5.218 5.218 0 005.232 5.215V1.431a1.005 1.005 0 00-1.005-1.005z\"\n />\n </svg>\n ),\n notion: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.98-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466l1.823 1.447zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.84-.046.933-.56.933-1.167V6.354c0-.606-.233-.933-.746-.886l-15.177.887c-.56.046-.747.326-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.746 0-.933-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.14c-.093-.514.28-.886.747-.933l3.222-.186zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.933.653.933 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.448-1.632z\" />\n </svg>\n ),\n default: (\n <svg\n className=\"w-6 h-6 text-neutral-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 10V3L4 14h7v7l9-11h-7z\"\n />\n </svg>\n ),\n };\n\n return iconMap[name] ?? iconMap.default;\n}\n",
|
|
107
|
+
"app/setup/page-helpers.tsx": "import type { JSX } from \"react\";\n\nexport interface Integration {\n id: string;\n name: string;\n icon: string;\n connected: boolean;\n connectUrl: string;\n}\n\nexport interface SetupStep {\n id: string;\n title: string;\n description: string;\n completed: boolean;\n action?: () => void;\n link?: string;\n}\n\ninterface SetupGuide {\n title: string;\n steps: string[];\n link: string;\n envVars: string[];\n category: string;\n}\n\nexport interface TokenStorageStatus {\n mode: \"memory\" | \"database\" | \"kv\" | \"redis\" | \"custom\";\n encrypted: boolean;\n autoGenerated?: boolean;\n}\n\nexport type TokenStorageStyles = {\n container: string;\n iconWrapper: string;\n title: string;\n text: string;\n isMemory: boolean;\n};\n\nexport const CATEGORIES = [\n { id: \"google\", name: \"Google Services\", icon: \"google\" },\n { id: \"microsoft\", name: \"Microsoft Services\", icon: \"microsoft\" },\n { id: \"atlassian\", name: \"Atlassian\", icon: \"atlassian\" },\n { id: \"communication\", name: \"Communication\", icon: \"chat\" },\n { id: \"development\", name: \"Development\", icon: \"code\" },\n { id: \"productivity\", name: \"Productivity\", icon: \"tasks\" },\n { id: \"storage\", name: \"Storage\", icon: \"folder\" },\n { id: \"infrastructure\", name: \"Infrastructure\", icon: \"server\" },\n { id: \"sales\", name: \"Sales & CRM\", icon: \"users\" },\n { id: \"support\", name: \"Support\", icon: \"headset\" },\n { id: \"finance\", name: \"Finance\", icon: \"dollar\" },\n { id: \"marketing\", name: \"Marketing\", icon: \"megaphone\" },\n { id: \"design\", name: \"Design\", icon: \"palette\" },\n { id: \"ai\", name: \"AI Providers\", icon: \"brain\" },\n] as const;\n\nexport const OAUTH_SETUP_GUIDES: Record<string, SetupGuide> = {\n gmail: {\n title: \"Google OAuth Setup (Gmail)\",\n category: \"google\",\n steps: [\n \"Go to Google Cloud Console\",\n \"Create a new project or select existing one\",\n \"Enable Gmail API in APIs & Services > Library\",\n \"Go to APIs & Services > Credentials\",\n \"Create OAuth 2.0 credentials (Web application)\",\n \"Add redirect URI: http://localhost:3000/api/auth/gmail/callback\",\n \"Copy Client ID and Secret to your .env file\",\n ],\n link: \"https://console.cloud.google.com/apis/credentials\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n calendar: {\n title: \"Google Calendar Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials as Gmail\",\n \"Enable Calendar API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/calendar/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/calendar-json.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n drive: {\n title: \"Google Drive Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Drive API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/drive/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/drive.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n sheets: {\n title: \"Google Sheets Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Sheets API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/sheets/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/sheets.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n \"docs-google\": {\n title: \"Google Docs Setup\",\n category: \"google\",\n steps: [\n \"Uses same Google OAuth credentials\",\n \"Enable Docs API in Google Cloud Console\",\n \"Add redirect URI: http://localhost:3000/api/auth/docs-google/callback\",\n ],\n link: \"https://console.cloud.google.com/apis/library/docs.googleapis.com\",\n envVars: [\"GOOGLE_CLIENT_ID\", \"GOOGLE_CLIENT_SECRET\"],\n },\n outlook: {\n title: \"Microsoft Outlook Setup\",\n category: \"microsoft\",\n steps: [\n \"Go to Azure Portal > Azure Active Directory\",\n \"Click App registrations > New registration\",\n \"Set redirect URI: http://localhost:3000/api/auth/outlook/callback\",\n \"Go to API permissions > Add Microsoft Graph permissions\",\n \"Add: Mail.Read, Mail.Send, Mail.ReadWrite\",\n \"Go to Certificates & secrets > New client secret\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n teams: {\n title: \"Microsoft Teams Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials as Outlook\",\n \"Add Teams permissions: Chat.Read, Chat.ReadWrite, Channel.ReadBasic.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/teams/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n onedrive: {\n title: \"Microsoft OneDrive Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Files.Read, Files.ReadWrite\",\n \"Add redirect URI: http://localhost:3000/api/auth/onedrive/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n sharepoint: {\n title: \"Microsoft SharePoint Setup\",\n category: \"microsoft\",\n steps: [\n \"Uses same Microsoft OAuth credentials\",\n \"Add permissions: Sites.Read.All, Sites.ReadWrite.All\",\n \"Add redirect URI: http://localhost:3000/api/auth/sharepoint/callback\",\n ],\n link: \"https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade\",\n envVars: [\"MICROSOFT_CLIENT_ID\", \"MICROSOFT_CLIENT_SECRET\"],\n },\n jira: {\n title: \"Atlassian Jira Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Atlassian Developer Console\",\n \"Click Create > OAuth 2.0 integration\",\n \"Add Jira API scopes: read:jira-work, write:jira-work\",\n \"Set callback URL: http://localhost:3000/api/auth/jira/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n confluence: {\n title: \"Atlassian Confluence Setup\",\n category: \"atlassian\",\n steps: [\n \"Uses same Atlassian OAuth credentials as Jira\",\n \"Add Confluence scopes: read:confluence-content.all, write:confluence-content\",\n \"Add callback URL: http://localhost:3000/api/auth/confluence/callback\",\n ],\n link: \"https://developer.atlassian.com/console/myapps/\",\n envVars: [\"ATLASSIAN_CLIENT_ID\", \"ATLASSIAN_CLIENT_SECRET\"],\n },\n bitbucket: {\n title: \"Atlassian Bitbucket Setup\",\n category: \"atlassian\",\n steps: [\n \"Go to Bitbucket Settings > OAuth consumers\",\n \"Click Add consumer\",\n \"Set callback URL: http://localhost:3000/api/auth/bitbucket/callback\",\n \"Add permissions: repository:read, repository:write\",\n \"Copy Key and Secret to .env\",\n ],\n link: \"https://bitbucket.org/account/settings/app-passwords/\",\n envVars: [\"BITBUCKET_CLIENT_ID\", \"BITBUCKET_CLIENT_SECRET\"],\n },\n slack: {\n title: \"Slack App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Slack API Apps page\",\n \"Click Create New App > From scratch\",\n \"Go to OAuth & Permissions\",\n \"Add scopes: channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, users:read\",\n \"Add redirect URL: http://localhost:3000/api/auth/slack/callback\",\n \"Install to Workspace\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://api.slack.com/apps\",\n envVars: [\"SLACK_CLIENT_ID\", \"SLACK_CLIENT_SECRET\"],\n },\n zoom: {\n title: \"Zoom App Setup\",\n category: \"communication\",\n steps: [\n \"Go to Zoom App Marketplace\",\n \"Click Develop > Build App\",\n \"Choose OAuth app type\",\n \"Add redirect URL: http://localhost:3000/api/auth/zoom/callback\",\n \"Add scopes: meeting:read, meeting:write, user:read\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://marketplace.zoom.us/develop/create\",\n envVars: [\"ZOOM_CLIENT_ID\", \"ZOOM_CLIENT_SECRET\"],\n },\n webex: {\n title: \"Webex Integration Setup\",\n category: \"communication\",\n steps: [\n \"Go to Webex Developer Portal\",\n \"Create a new integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/webex/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.webex.com/my-apps\",\n envVars: [\"WEBEX_CLIENT_ID\", \"WEBEX_CLIENT_SECRET\"],\n },\n twilio: {\n title: \"Twilio Setup\",\n category: \"communication\",\n steps: [\n \"Go to Twilio Console\",\n \"Copy Account SID and Auth Token\",\n \"Get a phone number for SMS\",\n \"Add credentials to .env\",\n ],\n link: \"https://console.twilio.com/\",\n envVars: [\"TWILIO_ACCOUNT_SID\", \"TWILIO_AUTH_TOKEN\", \"TWILIO_PHONE_NUMBER\"],\n },\n github: {\n title: \"GitHub OAuth App Setup\",\n category: \"development\",\n steps: [\n \"Go to GitHub Developer Settings\",\n \"Click OAuth Apps > New OAuth App\",\n \"Set Homepage URL: http://localhost:3000\",\n \"Set callback URL: http://localhost:3000/api/auth/github/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://github.com/settings/developers\",\n envVars: [\"GITHUB_CLIENT_ID\", \"GITHUB_CLIENT_SECRET\"],\n },\n gitlab: {\n title: \"GitLab OAuth Setup\",\n category: \"development\",\n steps: [\n \"Go to GitLab User Settings > Applications\",\n \"Create new application\",\n \"Add redirect URI: http://localhost:3000/api/auth/gitlab/callback\",\n \"Select scopes: api, read_user, read_repository\",\n \"Copy Application ID and Secret to .env\",\n ],\n link: \"https://gitlab.com/-/profile/applications\",\n envVars: [\"GITLAB_CLIENT_ID\", \"GITLAB_CLIENT_SECRET\"],\n },\n sentry: {\n title: \"Sentry Setup\",\n category: \"development\",\n steps: [\n \"Go to Sentry Settings > Developer Settings\",\n \"Create new integration\",\n \"Add redirect URL: http://localhost:3000/api/auth/sentry/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://sentry.io/settings/developer-settings/\",\n envVars: [\"SENTRY_CLIENT_ID\", \"SENTRY_CLIENT_SECRET\"],\n },\n posthog: {\n title: \"PostHog Setup\",\n category: \"development\",\n steps: [\"Go to PostHog Project Settings\", \"Copy your Project API Key\", \"Add to .env file\"],\n link: \"https://app.posthog.com/project/settings\",\n envVars: [\"POSTHOG_API_KEY\", \"POSTHOG_HOST\"],\n },\n mixpanel: {\n title: \"Mixpanel Setup\",\n category: \"development\",\n steps: [\n \"Go to Mixpanel Project Settings\",\n \"Copy your Project Token\",\n \"For API access, create a Service Account\",\n \"Add credentials to .env\",\n ],\n link: \"https://mixpanel.com/settings/project\",\n envVars: [\"MIXPANEL_TOKEN\", \"MIXPANEL_API_SECRET\"],\n },\n notion: {\n title: \"Notion Integration Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Notion Integrations page\",\n \"Click New integration\",\n \"Name your integration and select workspace\",\n \"Copy the Internal Integration Token\",\n \"Share desired pages/databases with your integration\",\n \"Add token to .env\",\n ],\n link: \"https://www.notion.so/my-integrations\",\n envVars: [\"NOTION_API_KEY\"],\n },\n linear: {\n title: \"Linear OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Linear Settings > API\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/linear/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://linear.app/settings/api\",\n envVars: [\"LINEAR_CLIENT_ID\", \"LINEAR_CLIENT_SECRET\"],\n },\n asana: {\n title: \"Asana OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Asana Developer Console\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/asana/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.asana.com/0/developer-console\",\n envVars: [\"ASANA_CLIENT_ID\", \"ASANA_CLIENT_SECRET\"],\n },\n trello: {\n title: \"Trello Power-Up Setup\",\n category: \"productivity\",\n steps: [\n \"Go to Trello Power-Ups Admin\",\n \"Create new Power-Up\",\n \"Add redirect URI: http://localhost:3000/api/auth/trello/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://trello.com/power-ups/admin\",\n envVars: [\"TRELLO_API_KEY\", \"TRELLO_API_SECRET\"],\n },\n monday: {\n title: \"Monday.com App Setup\",\n category: \"productivity\",\n steps: [\n \"Go to monday.com Developers\",\n \"Create new app\",\n \"Add OAuth redirect: http://localhost:3000/api/auth/monday/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://monday.com/developers/apps\",\n envVars: [\"MONDAY_CLIENT_ID\", \"MONDAY_CLIENT_SECRET\"],\n },\n clickup: {\n title: \"ClickUp OAuth Setup\",\n category: \"productivity\",\n steps: [\n \"Go to ClickUp Settings > Apps\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/clickup/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.clickup.com/settings/apps\",\n envVars: [\"CLICKUP_CLIENT_ID\", \"CLICKUP_CLIENT_SECRET\"],\n },\n box: {\n title: \"Box App Setup\",\n category: \"storage\",\n steps: [\n \"Go to Box Developer Console\",\n \"Create new app with OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/box/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://app.box.com/developers/console\",\n envVars: [\"BOX_CLIENT_ID\", \"BOX_CLIENT_SECRET\"],\n },\n airtable: {\n title: \"Airtable OAuth Setup\",\n category: \"storage\",\n steps: [\n \"Go to Airtable Developer Hub\",\n \"Create new OAuth integration\",\n \"Add redirect URI: http://localhost:3000/api/auth/airtable/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://airtable.com/create/oauth\",\n envVars: [\"AIRTABLE_CLIENT_ID\", \"AIRTABLE_CLIENT_SECRET\"],\n },\n supabase: {\n title: \"Supabase Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Supabase Dashboard\",\n \"Create new project or select existing\",\n \"Go to Settings > API\",\n \"Copy Project URL and anon/service_role keys\",\n \"Add to .env file\",\n ],\n link: \"https://supabase.com/dashboard\",\n envVars: [\"SUPABASE_URL\", \"SUPABASE_ANON_KEY\", \"SUPABASE_SERVICE_ROLE_KEY\"],\n },\n neon: {\n title: \"Neon Database Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Neon Console\",\n \"Create new project\",\n \"Copy connection string from Dashboard\",\n \"Add to .env file\",\n ],\n link: \"https://console.neon.tech/\",\n envVars: [\"DATABASE_URL\"],\n },\n snowflake: {\n title: \"Snowflake Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to Snowflake Console\",\n \"Create a service account or use existing credentials\",\n \"Note your account identifier, warehouse, database\",\n \"Add credentials to .env\",\n ],\n link: \"https://app.snowflake.com/\",\n envVars: [\"SNOWFLAKE_ACCOUNT\", \"SNOWFLAKE_USER\", \"SNOWFLAKE_PASSWORD\", \"SNOWFLAKE_WAREHOUSE\"],\n },\n aws: {\n title: \"AWS Setup\",\n category: \"infrastructure\",\n steps: [\n \"Go to AWS IAM Console\",\n \"Create new IAM user with programmatic access\",\n \"Attach required policies (S3, Lambda, DynamoDB)\",\n \"Copy Access Key ID and Secret\",\n \"Add to .env file\",\n ],\n link: \"https://console.aws.amazon.com/iam/\",\n envVars: [\"AWS_ACCESS_KEY_ID\", \"AWS_SECRET_ACCESS_KEY\", \"AWS_REGION\"],\n },\n salesforce: {\n title: \"Salesforce Connected App Setup\",\n category: \"sales\",\n steps: [\n \"Go to Salesforce Setup > App Manager\",\n \"Create new Connected App\",\n \"Enable OAuth Settings\",\n \"Add callback URL: http://localhost:3000/api/auth/salesforce/callback\",\n \"Select OAuth scopes: api, refresh_token\",\n \"Copy Consumer Key and Secret to .env\",\n ],\n link: \"https://login.salesforce.com/\",\n envVars: [\"SALESFORCE_CLIENT_ID\", \"SALESFORCE_CLIENT_SECRET\"],\n },\n pipedrive: {\n title: \"Pipedrive OAuth Setup\",\n category: \"sales\",\n steps: [\n \"Go to Pipedrive Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/pipedrive/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.pipedrive.com/\",\n envVars: [\"PIPEDRIVE_CLIENT_ID\", \"PIPEDRIVE_CLIENT_SECRET\"],\n },\n zendesk: {\n title: \"Zendesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Zendesk Admin > API > OAuth Clients\",\n \"Add new OAuth client\",\n \"Set redirect URI: http://localhost:3000/api/auth/zendesk/callback\",\n \"Copy Client ID and Secret to .env\",\n \"Add your Zendesk subdomain\",\n ],\n link: \"https://support.zendesk.com/hc/en-us/articles/4408845965210\",\n envVars: [\"ZENDESK_CLIENT_ID\", \"ZENDESK_CLIENT_SECRET\", \"ZENDESK_SUBDOMAIN\"],\n },\n intercom: {\n title: \"Intercom OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Intercom Developer Hub\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/intercom/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.intercom.com/\",\n envVars: [\"INTERCOM_CLIENT_ID\", \"INTERCOM_CLIENT_SECRET\"],\n },\n freshdesk: {\n title: \"Freshdesk OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to Freshdesk Admin > Apps > Custom Apps\",\n \"Create new OAuth application\",\n \"Add redirect URI: http://localhost:3000/api/auth/freshdesk/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developers.freshdesk.com/\",\n envVars: [\"FRESHDESK_CLIENT_ID\", \"FRESHDESK_CLIENT_SECRET\", \"FRESHDESK_DOMAIN\"],\n },\n servicenow: {\n title: \"ServiceNow OAuth Setup\",\n category: \"support\",\n steps: [\n \"Go to ServiceNow System OAuth > Application Registry\",\n \"Create OAuth API endpoint for external clients\",\n \"Add redirect URL: http://localhost:3000/api/auth/servicenow/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://docs.servicenow.com/\",\n envVars: [\"SERVICENOW_CLIENT_ID\", \"SERVICENOW_CLIENT_SECRET\", \"SERVICENOW_INSTANCE\"],\n },\n stripe: {\n title: \"Stripe Setup\",\n category: \"finance\",\n steps: [\n \"Go to Stripe Dashboard\",\n \"Go to Developers > API keys\",\n \"Copy Publishable and Secret keys\",\n \"For Connect, set up OAuth in Connect settings\",\n \"Add to .env file\",\n ],\n link: \"https://dashboard.stripe.com/apikeys\",\n envVars: [\"STRIPE_SECRET_KEY\", \"STRIPE_PUBLISHABLE_KEY\"],\n },\n quickbooks: {\n title: \"QuickBooks OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Intuit Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/quickbooks/callback\",\n \"Select Accounting scope\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.intuit.com/app/developer/dashboard\",\n envVars: [\"QUICKBOOKS_CLIENT_ID\", \"QUICKBOOKS_CLIENT_SECRET\"],\n },\n xero: {\n title: \"Xero OAuth Setup\",\n category: \"finance\",\n steps: [\n \"Go to Xero Developer Portal\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/xero/callback\",\n \"Select required scopes\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.xero.com/app/manage\",\n envVars: [\"XERO_CLIENT_ID\", \"XERO_CLIENT_SECRET\"],\n },\n mailchimp: {\n title: \"Mailchimp OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Mailchimp Developer Portal\",\n \"Register new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/mailchimp/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://admin.mailchimp.com/account/oauth2/\",\n envVars: [\"MAILCHIMP_CLIENT_ID\", \"MAILCHIMP_CLIENT_SECRET\"],\n },\n shopify: {\n title: \"Shopify App Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Shopify Partners Dashboard\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/shopify/callback\",\n \"Copy API Key and Secret to .env\",\n ],\n link: \"https://partners.shopify.com/\",\n envVars: [\"SHOPIFY_API_KEY\", \"SHOPIFY_API_SECRET\"],\n },\n twitter: {\n title: \"Twitter/X OAuth Setup\",\n category: \"marketing\",\n steps: [\n \"Go to Twitter Developer Portal\",\n \"Create new project and app\",\n \"Enable OAuth 2.0\",\n \"Add redirect URI: http://localhost:3000/api/auth/twitter/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://developer.twitter.com/en/portal/dashboard\",\n envVars: [\"TWITTER_CLIENT_ID\", \"TWITTER_CLIENT_SECRET\"],\n },\n figma: {\n title: \"Figma OAuth Setup\",\n category: \"design\",\n steps: [\n \"Go to Figma Developer Settings\",\n \"Create new app\",\n \"Add redirect URI: http://localhost:3000/api/auth/figma/callback\",\n \"Copy Client ID and Secret to .env\",\n ],\n link: \"https://www.figma.com/developers/apps\",\n envVars: [\"FIGMA_CLIENT_ID\", \"FIGMA_CLIENT_SECRET\"],\n },\n anthropic: {\n title: \"Anthropic API Setup\",\n category: \"ai\",\n steps: [\"Go to Anthropic Console\", \"Create new API key\", \"Copy API key to .env\"],\n link: \"https://console.anthropic.com/\",\n envVars: [\"ANTHROPIC_API_KEY\"],\n },\n};\n\nexport function filterIntegrations(\n integrations: Integration[],\n searchQuery: string,\n selectedCategory: string | null,\n): Integration[] {\n const query = searchQuery.toLowerCase();\n\n return integrations.filter((integration) => {\n const guide = OAUTH_SETUP_GUIDES[integration.id];\n\n const matchesSearch =\n query === \"\" ||\n integration.name.toLowerCase().includes(query) ||\n integration.id.toLowerCase().includes(query);\n\n const matchesCategory = selectedCategory === null || guide?.category === selectedCategory;\n\n return matchesSearch && matchesCategory;\n });\n}\n\nexport function groupIntegrationsByCategory(\n integrations: Integration[],\n): Record<string, Integration[]> {\n const groups: Record<string, Integration[]> = {};\n\n for (const integration of integrations) {\n const category = OAUTH_SETUP_GUIDES[integration.id]?.category ?? \"other\";\n (groups[category] ??= []).push(integration);\n }\n\n return groups;\n}\n\nexport function buildSetupSteps(\n envChecked: boolean,\n allConnected: boolean,\n markEnvChecked: () => void,\n): SetupStep[] {\n return [\n {\n id: \"env\",\n title: \"Configure Environment Variables\",\n description: \"Add your OAuth credentials to the .env file\",\n completed: envChecked,\n action: markEnvChecked,\n },\n {\n id: \"oauth\",\n title: \"Create OAuth Apps\",\n description: \"Set up OAuth applications for each service\",\n completed: false,\n },\n {\n id: \"connect\",\n title: \"Connect Services\",\n description: \"Authorize your app to access each service\",\n completed: allConnected,\n },\n ];\n}\n\nexport function getTokenStorageStyles(\n tokenStorage: TokenStorageStatus | null,\n): TokenStorageStyles | null {\n if (!tokenStorage) return null;\n\n const isMemory = tokenStorage.mode === \"memory\";\n\n return {\n container: `rounded-2xl p-6 shadow-sm border mb-8 ${\n isMemory\n ? \"bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-800\"\n : \"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800\"\n }`,\n iconWrapper: `w-10 h-10 rounded-full flex items-center justify-center ${\n isMemory ? \"bg-amber-100 dark:bg-amber-900\" : \"bg-green-100 dark:bg-green-900\"\n }`,\n title: `font-semibold ${\n isMemory ? \"text-amber-800 dark:text-amber-200\" : \"text-green-800 dark:text-green-200\"\n }`,\n text: `text-sm mt-1 ${\n isMemory ? \"text-amber-700 dark:text-amber-300\" : \"text-green-700 dark:text-green-300\"\n }`,\n isMemory,\n };\n}\n\nexport function ServiceIcon({ name }: { name: string }): React.JSX.Element {\n const iconMap: Record<string, React.JSX.Element> = {\n mail: (\n <svg className=\"w-6 h-6 text-red-500\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n fill=\"none\"\n />\n </svg>\n ),\n slack: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z\"\n fill=\"#E01E5A\"\n />\n <path\n d=\"M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z\"\n fill=\"#36C5F0\"\n />\n <path\n d=\"M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312z\"\n fill=\"#2EB67D\"\n />\n <path\n d=\"M15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\"\n fill=\"#ECB22E\"\n />\n </svg>\n ),\n calendar: (\n <svg\n className=\"w-6 h-6 text-blue-500\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"\n />\n </svg>\n ),\n github: (\n <svg className=\"w-6 h-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n fillRule=\"evenodd\"\n d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\"\n clipRule=\"evenodd\"\n />\n </svg>\n ),\n jira: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\">\n <defs>\n <linearGradient id=\"jira-gradient\" x1=\"98.031%\" x2=\"58.888%\" y1=\".161%\" y2=\"40.766%\">\n <stop offset=\"0%\" stopColor=\"#0052CC\" />\n <stop offset=\"100%\" stopColor=\"#2684FF\" />\n </linearGradient>\n </defs>\n <path\n fill=\"url(#jira-gradient)\"\n d=\"M11.571 11.513H0a5.218 5.218 0 005.232 5.215h2.13v2.057A5.215 5.215 0 0012.575 24V12.518a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M17.151 5.97H5.58a5.215 5.215 0 005.215 5.214h2.129v2.058a5.218 5.218 0 005.232 5.215V6.975a1.005 1.005 0 00-1.005-1.005z\"\n />\n <path\n fill=\"#2684FF\"\n d=\"M22.723.426H11.152a5.215 5.215 0 005.215 5.215h2.129v2.057a5.218 5.218 0 005.232 5.215V1.431a1.005 1.005 0 00-1.005-1.005z\"\n />\n </svg>\n ),\n notion: (\n <svg className=\"w-6 h-6\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.98-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466l1.823 1.447zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.84-.046.933-.56.933-1.167V6.354c0-.606-.233-.933-.746-.886l-15.177.887c-.56.046-.747.326-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.746 0-.933-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.14c-.093-.514.28-.886.747-.933l3.222-.186zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.933.653.933 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.448-1.632z\" />\n </svg>\n ),\n default: (\n <svg\n className=\"w-6 h-6 text-neutral-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 10V3L4 14h7v7l9-11h-7z\"\n />\n </svg>\n ),\n };\n\n return iconMap[name] ?? iconMap.default;\n}\n",
|
|
108
108
|
"app/setup/page.tsx": "\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n buildSetupSteps,\n CATEGORIES,\n filterIntegrations,\n getTokenStorageStyles,\n groupIntegrationsByCategory,\n type Integration,\n OAUTH_SETUP_GUIDES,\n ServiceIcon,\n type TokenStorageStatus,\n} from \"./page-helpers\";\n\nexport default function SetupPage(): React.JSX.Element {\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [loading, setLoading] = useState(true);\n const [expandedGuide, setExpandedGuide] = useState<string | null>(null);\n const [envChecked, setEnvChecked] = useState(false);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [selectedCategory, setSelectedCategory] = useState<string | null>(null);\n const [tokenStorage, setTokenStorage] = useState<TokenStorageStatus | null>(null);\n\n useEffect(() => {\n void fetchStatus();\n void fetchTokenStorage();\n }, []);\n\n async function fetchStatus(): Promise<void> {\n try {\n const res = await fetch(\"/api/integrations/status\");\n if (!res.ok) {\n console.error(\"Failed to fetch integration status:\", res.status);\n setIntegrations([]);\n return;\n }\n\n const data = await res.json();\n setIntegrations(data.integrations ?? []);\n } catch (error) {\n console.error(\"Failed to fetch integration status:\", error);\n setIntegrations([]);\n } finally {\n setLoading(false);\n }\n }\n\n async function fetchTokenStorage(): Promise<void> {\n const fallback: TokenStorageStatus = { mode: \"memory\", encrypted: false };\n\n try {\n const res = await fetch(\"/api/integrations/token-storage\");\n if (!res.ok) {\n setTokenStorage(fallback);\n return;\n }\n const data = await res.json();\n setTokenStorage(data);\n } catch {\n setTokenStorage(fallback);\n }\n }\n\n const filteredIntegrations = useMemo(\n () => filterIntegrations(integrations, searchQuery, selectedCategory),\n [integrations, searchQuery, selectedCategory],\n );\n\n const groupedIntegrations = useMemo(\n () => groupIntegrationsByCategory(filteredIntegrations),\n [filteredIntegrations],\n );\n\n const connectedCount = integrations.filter((i) => i.connected).length;\n const totalCount = integrations.length;\n const progress = totalCount > 0 ? (connectedCount / totalCount) * 100 : 0;\n\n const allConnected = connectedCount === totalCount && totalCount > 0;\n\n const setupSteps = useMemo(\n () => buildSetupSteps(envChecked, allConnected, () => setEnvChecked(true)),\n [allConnected, envChecked],\n );\n\n const tokenStorageStyles = useMemo(() => getTokenStorageStyles(tokenStorage), [tokenStorage]);\n\n return (\n <div className=\"min-h-screen bg-neutral-50 dark:bg-neutral-900\">\n <div className=\"max-w-4xl mx-auto px-4 py-12\">\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-neutral-900 dark:text-white mb-4\">\n Setup Your AI Agent\n </h1>\n <p className=\"text-lg text-neutral-600 dark:text-neutral-400\">\n Connect your services to enable AI-powered automation\n </p>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl p-6 shadow-sm border border-neutral-200 dark:border-neutral-700 mb-8\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm font-medium text-neutral-600 dark:text-neutral-400\">\n Setup Progress\n </span>\n <span className=\"text-sm font-medium text-neutral-900 dark:text-white\">\n {connectedCount} / {totalCount} services connected\n </span>\n </div>\n <div className=\"w-full bg-neutral-200 dark:bg-neutral-700 rounded-full h-3\">\n <div\n className=\"bg-gradient-to-r from-green-500 to-emerald-500 h-3 rounded-full transition-all duration-500\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n\n {tokenStorage && tokenStorageStyles && (\n <div className={tokenStorageStyles.container}>\n <div className=\"flex items-start gap-4\">\n <div className={tokenStorageStyles.iconWrapper}>\n {tokenStorageStyles.isMemory ? (\n <svg\n className=\"w-5 h-5 text-amber-600 dark:text-amber-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n ) : (\n <svg\n className=\"w-5 h-5 text-green-600 dark:text-green-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n )}\n </div>\n\n <div className=\"flex-1\">\n <h3 className={tokenStorageStyles.title}>\n Token Storage:{\" \"}\n {tokenStorageStyles.isMemory\n ? \"Development Mode\"\n : `${tokenStorage.mode.charAt(0).toUpperCase()}${tokenStorage.mode.slice(\n 1,\n )} Storage`}\n </h3>\n\n <p className={tokenStorageStyles.text}>\n {tokenStorageStyles.isMemory ? (\n <>Tokens are stored in memory and will be lost on restart.</>\n ) : (\n <>Tokens are persisted to {tokenStorage.mode} storage.</>\n )}\n </p>\n\n <div className=\"mt-2 flex items-center gap-1.5 text-sm text-green-600 dark:text-green-400\">\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"\n />\n </svg>\n <span>Encryption enabled {tokenStorage.autoGenerated && \"(auto-generated key)\"}</span>\n </div>\n\n {tokenStorageStyles.isMemory && (\n <div className=\"mt-4 pt-4 border-t border-amber-200 dark:border-amber-800\">\n <p className=\"text-sm font-medium text-amber-800 dark:text-amber-200 mb-3\">\n For production, add one of these to your{\" \"}\n <code className=\"px-1 py-0.5 bg-amber-100 dark:bg-amber-900 rounded text-xs\">\n .env\n </code>\n :\n </p>\n <div className=\"grid gap-2\">\n <a\n href=\"https://upstash.com/docs/redis/overall/getstarted\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-green-200 dark:border-green-700 hover:border-green-400 dark:hover:border-green-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Upstash\n </span>\n <span className=\"text-green-600 dark:text-green-400 text-xs ml-2 font-medium\">\n Recommended\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Serverless Redis, scales horizontally\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n REDIS_URL\n </code>\n </a>\n\n <a\n href=\"https://docs.turso.tech/quickstart\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Turso / libSQL\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Edge SQLite, fast reads globally\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL\n </code>\n </a>\n\n <a\n href=\"https://vercel.com/docs/storage/vercel-kv/quickstart\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n Vercel KV\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Built-in if using Vercel\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n KV_REST_API_URL\n </code>\n </a>\n\n <a\n href=\"https://neon.tech/docs/get-started-with-neon/connect-neon\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">Neon</span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Serverless Postgres\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL\n </code>\n </a>\n\n <a\n href=\"https://www.sqlite.org/index.html\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-amber-200 dark:border-amber-700 hover:border-amber-400 dark:hover:border-amber-500 transition-colors group\"\n >\n <div>\n <span className=\"font-medium text-neutral-900 dark:text-white\">\n SQLite\n </span>\n <span className=\"text-neutral-500 dark:text-neutral-400 text-sm ml-2\">\n Local file, single instance only\n </span>\n </div>\n <code className=\"text-xs bg-neutral-100 dark:bg-neutral-700 px-2 py-1 rounded text-neutral-600 dark:text-neutral-300\">\n DATABASE_URL=file:./data.db\n </code>\n </a>\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n )}\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl shadow-sm border border-neutral-200 dark:border-neutral-700 mb-8 overflow-hidden\">\n <div className=\"p-6 border-b border-neutral-200 dark:border-neutral-700\">\n <h2 className=\"text-xl font-semibold text-neutral-900 dark:text-white\">\n Quick Start Guide\n </h2>\n </div>\n <div className=\"divide-y divide-neutral-200 dark:divide-neutral-700\">\n {setupSteps.map((step, index) => (\n <div key={step.id} className=\"p-6 flex items-start gap-4\">\n <div\n className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${\n step.completed\n ? \"bg-green-500 text-white\"\n : \"bg-neutral-200 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400\"\n }`}\n >\n {step.completed ? (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n ) : (\n <span className=\"font-semibold\">{index + 1}</span>\n )}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"font-semibold text-neutral-900 dark:text-white\">{step.title}</h3>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-400 mt-1\">\n {step.description}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n\n <div className=\"bg-white dark:bg-neutral-800 rounded-2xl shadow-sm border border-neutral-200 dark:border-neutral-700 overflow-hidden\">\n <div className=\"p-6 border-b border-neutral-200 dark:border-neutral-700\">\n <h2 className=\"text-xl font-semibold text-neutral-900 dark:text-white\">\n Service Connections\n </h2>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-400 mt-1\">\n Click on a service to see setup instructions or connect\n </p>\n\n <div className=\"mt-4\">\n <input\n type=\"text\"\n placeholder=\"Search services...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full px-4 py-2 bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl text-neutral-900 dark:text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n </div>\n\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setSelectedCategory(null)}\n className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${\n selectedCategory === null\n ? \"bg-neutral-900 dark:bg-white text-white dark:text-neutral-900\"\n : \"bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600\"\n }`}\n >\n All\n </button>\n\n {CATEGORIES.map((category) => (\n <button\n key={category.id}\n type=\"button\"\n onClick={() =>\n setSelectedCategory(selectedCategory === category.id ? null : category.id)\n }\n className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${\n selectedCategory === category.id\n ? \"bg-neutral-900 dark:bg-white text-white dark:text-neutral-900\"\n : \"bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600\"\n }`}\n >\n {category.name}\n </button>\n ))}\n </div>\n </div>\n\n {loading ? (\n <div className=\"p-12 text-center text-neutral-500\">Loading...</div>\n ) : filteredIntegrations.length === 0 ? (\n <div className=\"p-12 text-center text-neutral-500\">\n No services found matching your search\n </div>\n ) : (\n <div>\n {CATEGORIES.filter((cat) => groupedIntegrations[cat.id]?.length > 0).map(\n (category) => (\n <div key={category.id}>\n <div className=\"px-6 py-3 bg-neutral-50 dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-700\">\n <h3 className=\"text-sm font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider\">\n {category.name}\n </h3>\n </div>\n\n <div className=\"divide-y divide-neutral-200 dark:divide-neutral-700\">\n {groupedIntegrations[category.id]?.map((integration) => {\n const guide = OAUTH_SETUP_GUIDES[integration.id];\n const isExpanded = expandedGuide === integration.id;\n\n return (\n <div key={integration.id}>\n <div className=\"p-6 flex items-center justify-between\">\n <div className=\"flex items-center gap-4\">\n <div className=\"w-12 h-12 bg-neutral-100 dark:bg-neutral-700 rounded-xl flex items-center justify-center\">\n <ServiceIcon name={integration.icon} />\n </div>\n <div>\n <h3 className=\"font-semibold text-neutral-900 dark:text-white\">\n {integration.name}\n </h3>\n <p\n className={`text-sm ${\n integration.connected\n ? \"text-green-600 dark:text-green-400\"\n : \"text-neutral-500\"\n }`}\n >\n {integration.connected ? \"Connected\" : \"Not connected\"}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3\">\n {guide && (\n <button\n type=\"button\"\n onClick={() =>\n setExpandedGuide(isExpanded ? null : integration.id)\n }\n className=\"px-4 py-2 text-sm font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white\"\n >\n {isExpanded ? \"Hide Guide\" : \"Setup Guide\"}\n </button>\n )}\n\n {integration.connected ? (\n <span className=\"inline-flex items-center gap-1.5 px-4 py-2 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 rounded-xl text-sm font-medium\">\n <span className=\"w-2 h-2 bg-green-500 rounded-full\" />\n Connected\n </span>\n ) : (\n <a\n href={integration.connectUrl}\n className=\"px-4 py-2 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 rounded-xl text-sm font-medium hover:opacity-90 transition-opacity\"\n >\n Connect\n </a>\n )}\n </div>\n </div>\n\n {isExpanded && guide && (\n <div className=\"px-6 pb-6\">\n <div className=\"bg-neutral-50 dark:bg-neutral-900 rounded-xl p-6 border border-neutral-200 dark:border-neutral-700\">\n <h4 className=\"font-semibold text-neutral-900 dark:text-white mb-4\">\n {guide.title}\n </h4>\n\n <ol className=\"space-y-3 mb-6\">\n {guide.steps.map((step, i) => (\n <li key={i} className=\"flex items-start gap-3\">\n <span className=\"w-6 h-6 bg-neutral-200 dark:bg-neutral-700 rounded-full flex items-center justify-center text-sm font-medium text-neutral-600 dark:text-neutral-400 flex-shrink-0\">\n {i + 1}\n </span>\n <span className=\"text-neutral-700 dark:text-neutral-300\">\n {step}\n </span>\n </li>\n ))}\n </ol>\n\n <div className=\"mb-4 p-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg\">\n <h5 className=\"text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-2\">\n Required Environment Variables:\n </h5>\n <pre className=\"text-sm text-neutral-600 dark:text-neutral-400 font-mono whitespace-pre-wrap\">\n {guide.envVars.map((v) => `${v}=your_value`).join(\"\\n\")}\n </pre>\n </div>\n\n <a\n href={guide.link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 text-sm font-medium hover:underline\"\n >\n Open Developer Console\n <svg\n className=\"w-4 h-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n />\n </svg>\n </a>\n </div>\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ),\n )}\n </div>\n )}\n </div>\n\n {allConnected && (\n <div className=\"mt-8 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-2xl p-6 border border-green-200 dark:border-green-800 text-center\">\n <div className=\"text-4xl mb-4\">🎉</div>\n <h3 className=\"text-xl font-semibold text-green-800 dark:text-green-200 mb-2\">\n All Services Connected!\n </h3>\n <p className=\"text-green-700 dark:text-green-300 mb-4\">\n Your AI agent is ready to use. Start chatting to automate your workflows.\n </p>\n <a\n href=\"/\"\n className=\"inline-flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-xl font-medium hover:bg-green-700 transition-colors\"\n >\n Start Using Your Agent\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 7l5 5m0 0l-5 5m5-5H6\"\n />\n </svg>\n </a>\n </div>\n )}\n </div>\n </div>\n );\n}\n",
|
|
109
109
|
"lib/oauth-memory-store.ts": "import { MemoryTokenStore } from \"veryfront/oauth\";\n\nexport const oauthMemoryTokenStore = new MemoryTokenStore();\n",
|
|
110
110
|
"lib/oauth.ts": "import { type OAuthToken, tokenStore } from \"./token-store.ts\";\n\nexport interface OAuthProvider {\n name: string;\n authorizationUrl: string;\n tokenUrl: string;\n clientId: string;\n clientSecret: string;\n scopes: string[];\n callbackPath: string;\n}\n\nfunction getExpiresAt(expiresIn: unknown): number | undefined {\n if (typeof expiresIn !== \"number\" || expiresIn <= 0) return undefined;\n return Date.now() + expiresIn * 1000;\n}\n\nasync function postTokenRequest(\n provider: OAuthProvider,\n body: Record<string, string>,\n errorPrefix: string,\n): Promise<any> {\n const response = await fetch(provider.tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(body),\n });\n\n if (response.ok) return response.json();\n\n const error = await response.text();\n throw new Error(`${errorPrefix}: ${response.status} - ${error}`);\n}\n\nexport function getAuthorizationUrl(\n provider: OAuthProvider,\n state: string,\n redirectUri: string,\n): string {\n const params = new URLSearchParams({\n client_id: provider.clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: provider.scopes.join(\" \"),\n state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n\n return `${provider.authorizationUrl}?${params.toString()}`;\n}\n\nexport async function exchangeCodeForTokens(\n provider: OAuthProvider,\n code: string,\n redirectUri: string,\n): Promise<OAuthToken> {\n const data = await postTokenRequest(\n provider,\n {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n code,\n grant_type: \"authorization_code\",\n redirect_uri: redirectUri,\n },\n \"Token exchange failed\",\n );\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function refreshAccessToken(\n provider: OAuthProvider,\n refreshToken: string,\n): Promise<OAuthToken> {\n const data = await postTokenRequest(\n provider,\n {\n client_id: provider.clientId,\n client_secret: provider.clientSecret,\n refresh_token: refreshToken,\n grant_type: \"refresh_token\",\n },\n \"Token refresh failed\",\n );\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token ?? refreshToken,\n expiresAt: getExpiresAt(data.expires_in),\n tokenType: data.token_type ?? \"Bearer\",\n scope: data.scope,\n };\n}\n\nexport async function getValidToken(\n provider: OAuthProvider,\n userId: string,\n service: string,\n): Promise<string | null> {\n const token = await tokenStore.getToken(userId, service);\n if (!token) return null;\n\n const isExpired = token.expiresAt\n ? token.expiresAt < Date.now() + 5 * 60 * 1000\n : false;\n\n if (!isExpired || !token.refreshToken) return token.accessToken;\n\n try {\n const newToken = await refreshAccessToken(provider, token.refreshToken);\n await tokenStore.setToken(userId, service, newToken);\n return newToken.accessToken;\n } catch {\n await tokenStore.revokeToken(userId, service);\n return null;\n }\n}\n",
|
package/esm/deno.js
CHANGED
|
@@ -80,11 +80,11 @@ export interface UseChatResult {
|
|
|
80
80
|
addToolOutput: (output: ToolOutput) => void;
|
|
81
81
|
data?: unknown;
|
|
82
82
|
handleInputChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
83
|
-
handleSubmit: (e
|
|
83
|
+
handleSubmit: (e?: React.FormEvent) => Promise<void>;
|
|
84
84
|
/** Alias for `handleInputChange` — matches `ChatProps.onChange` for easy spreading */
|
|
85
85
|
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
86
86
|
/** Alias for `handleSubmit` — matches `ChatProps.onSubmit` for easy spreading */
|
|
87
|
-
onSubmit: (e
|
|
87
|
+
onSubmit: (e?: React.FormEvent) => Promise<void>;
|
|
88
88
|
/** Alias for `setModel` — matches `ChatProps.onModelChange` for easy spreading */
|
|
89
89
|
onModelChange: (model: string | undefined) => void;
|
|
90
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/src/agent/react/use-chat/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAEnC,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,cAAc,GAAG,SAAS,CAAC;AAEjE,sDAAsD;AACtD,MAAM,MAAM,sBAAsB,GAC9B,MAAM,GACN,iBAAiB,GACjB,mBAAmB,GACnB,OAAO,GACP,YAAY,GACZ,OAAO,CAAC;AAEZ,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,GACd,CAAC;AAEF,KAAK,eAAe,GAAG,kBAAkB,GAAG,cAAc,CAAC;AAE3D,wBAAwB;AACxB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,oCAAoC;AACpC,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iGAAiG;IACjG,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,6CAA6C;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,yFAAyF;IACzF,aAAa,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,yEAAyE;IACzE,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,yFAAyF;IACzF,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,UAAU,CAAC;IAC/C,sDAAsD;IACtD,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC/C,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,iBAAiB,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;IAC1F,YAAY,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/src/agent/react/use-chat/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,2BAA2B,CAAC;AAEnC,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,cAAc,GAAG,SAAS,CAAC;AAEjE,sDAAsD;AACtD,MAAM,MAAM,sBAAsB,GAC9B,MAAM,GACN,iBAAiB,GACjB,mBAAmB,GACnB,OAAO,GACP,YAAY,GACZ,OAAO,CAAC;AAEZ,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,GACd,CAAC;AAEF,KAAK,eAAe,GAAG,kBAAkB,GAAG,cAAc,CAAC;AAE3D,wBAAwB;AACxB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,oCAAoC;AACpC,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,WAAW,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iGAAiG;IACjG,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,6CAA6C;IAC7C,aAAa,EAAE,aAAa,CAAC;IAC7B,yFAAyF;IACzF,aAAa,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,yEAAyE;IACzE,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,yFAAyF;IACzF,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,UAAU,CAAC;IAC/C,sDAAsD;IACtD,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC/C,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,iBAAiB,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;IAC1F,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,sFAAsF;IACtF,QAAQ,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;IACjF,iFAAiF;IACjF,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,kFAAkF;IAClF,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CACpD"}
|