@askable-ui/create-app 0.6.3
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/README.md +25 -0
- package/bin/create-askable-app.js +7 -0
- package/package.json +32 -0
- package/src/scaffold.js +80 -0
- package/template/.env.example +5 -0
- package/template/README.md +57 -0
- package/template/_gitignore +4 -0
- package/template/index.html +12 -0
- package/template/package.json +40 -0
- package/template/server/index.ts +55 -0
- package/template/src/App.tsx +254 -0
- package/template/src/main.tsx +16 -0
- package/template/src/styles.css +304 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +21 -0
- package/template/tsconfig.server.json +16 -0
- package/template/vite.config.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @askable-ui/create-app
|
|
2
|
+
|
|
3
|
+
Scaffold a runnable **React + Vite + CopilotKit + askable-ui** starter app.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create @askable-ui/app my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The generated app includes:
|
|
15
|
+
|
|
16
|
+
- a small analytics dashboard annotated with `data-askable`
|
|
17
|
+
- `useAskable()` pre-wired for hover + click focus tracking
|
|
18
|
+
- `useCopilotReadable()` pushing current and recent Askable context into CopilotKit
|
|
19
|
+
- a local Express runtime wired to `@copilotkit/runtime`
|
|
20
|
+
- a starter README and `.env.example`
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
- The scaffold writes files only. It does **not** auto-run `npm install`.
|
|
25
|
+
- If `OPENAI_API_KEY` is missing, the generated runtime still starts so the app boots locally, but Copilot responses stay disabled until you add a real key.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@askable-ui/create-app",
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "Scaffold a React + Vite + CopilotKit + askable-ui starter app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-askable-app": "bin/create-askable-app.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"template",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/askable-ui/askable.git",
|
|
18
|
+
"directory": "packages/create-askable-app"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"askable",
|
|
22
|
+
"copilotkit",
|
|
23
|
+
"vite",
|
|
24
|
+
"react",
|
|
25
|
+
"scaffold"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://askable-ui.com",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const ASKABLE_VERSION = '0.6.3';
|
|
6
|
+
const COPILOTKIT_VERSION = '1.56.2';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const TEMPLATE_DIR = path.resolve(__dirname, '..', 'template');
|
|
11
|
+
|
|
12
|
+
export function toPackageName(rawName) {
|
|
13
|
+
return rawName
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9-_]+/g, '-')
|
|
17
|
+
.replace(/^-+|-+$/g, '')
|
|
18
|
+
.replace(/-{2,}/g, '-');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isDirectoryEmpty(targetDir) {
|
|
22
|
+
if (!fs.existsSync(targetDir)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entries = fs.readdirSync(targetDir).filter((entry) => entry !== '.DS_Store');
|
|
27
|
+
return entries.length === 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function render(content, projectName) {
|
|
31
|
+
return content
|
|
32
|
+
.replaceAll('__APP_NAME__', projectName)
|
|
33
|
+
.replaceAll('__PACKAGE_NAME__', toPackageName(projectName) || 'askable-app')
|
|
34
|
+
.replaceAll('__ASKABLE_VERSION__', ASKABLE_VERSION)
|
|
35
|
+
.replaceAll('__COPILOTKIT_VERSION__', COPILOTKIT_VERSION);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyTemplate(templateDir, targetDir, projectName) {
|
|
39
|
+
for (const entry of fs.readdirSync(templateDir, { withFileTypes: true })) {
|
|
40
|
+
const sourcePath = path.join(templateDir, entry.name);
|
|
41
|
+
const outputName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
42
|
+
const targetPath = path.join(targetDir, outputName);
|
|
43
|
+
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
46
|
+
copyTemplate(sourcePath, targetPath, projectName);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = fs.readFileSync(sourcePath, 'utf8');
|
|
51
|
+
fs.writeFileSync(targetPath, render(raw, projectName));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runCli(args) {
|
|
56
|
+
const [projectArg] = args;
|
|
57
|
+
|
|
58
|
+
if (!projectArg || projectArg === '--help' || projectArg === '-h') {
|
|
59
|
+
console.log(`create-askable-app\n\nUsage:\n npm create @askable-ui/app my-app\n`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const projectName = projectArg.trim();
|
|
64
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
65
|
+
|
|
66
|
+
if (!isDirectoryEmpty(targetDir)) {
|
|
67
|
+
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
71
|
+
copyTemplate(TEMPLATE_DIR, targetDir, projectName);
|
|
72
|
+
|
|
73
|
+
console.log(`\n✔ askable starter created at ${targetDir}\n`);
|
|
74
|
+
console.log('Next steps:');
|
|
75
|
+
console.log(` cd ${projectName}`);
|
|
76
|
+
console.log(' npm install');
|
|
77
|
+
console.log(' cp .env.example .env');
|
|
78
|
+
console.log(' npm run dev');
|
|
79
|
+
console.log('\nTip: add OPENAI_API_KEY to .env when you want the CopilotKit runtime to answer.');
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A starter project generated by `create-askable-app`.
|
|
4
|
+
|
|
5
|
+
This template wires together:
|
|
6
|
+
|
|
7
|
+
- **askable-ui** for UI focus tracking with `useAskable()`
|
|
8
|
+
- **CopilotKit** for the chat sidebar and runtime
|
|
9
|
+
- **React + Vite** for the frontend
|
|
10
|
+
- **Express + @copilotkit/runtime** for the local backend runtime
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
cp .env.example .env
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The app opens at `http://localhost:5173` and the Copilot runtime listens on `http://localhost:3001/api/copilotkit`.
|
|
21
|
+
|
|
22
|
+
## Environment
|
|
23
|
+
|
|
24
|
+
Update `.env` with your own provider key:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
OPENAI_API_KEY=<your key>
|
|
28
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If `OPENAI_API_KEY` is blank, the runtime still starts so the dashboard loads, but Copilot responses stay disabled until you add a real key.
|
|
32
|
+
|
|
33
|
+
## How Askable and CopilotKit fit together
|
|
34
|
+
|
|
35
|
+
1. Dashboard panels are annotated with `data-askable` metadata and curated `data-askable-text` summaries.
|
|
36
|
+
2. `useAskable()` observes the page and produces prompt-ready context like:
|
|
37
|
+
- current focus
|
|
38
|
+
- recent interaction history
|
|
39
|
+
3. `useCopilotReadable()` forwards those Askable strings into CopilotKit.
|
|
40
|
+
4. CopilotKit sends the chat request to the local runtime at `/api/copilotkit`.
|
|
41
|
+
5. The runtime calls the LLM and answers with the current dashboard context already attached.
|
|
42
|
+
|
|
43
|
+
## Starter structure
|
|
44
|
+
|
|
45
|
+
- `src/App.tsx` — annotated dashboard + `useCopilotReadable()` wiring
|
|
46
|
+
- `src/styles.css` — dashboard styling
|
|
47
|
+
- `server/index.ts` — local CopilotKit runtime
|
|
48
|
+
- `.env.example` — runtime URL, OpenAI key, model, and CORS settings
|
|
49
|
+
|
|
50
|
+
## Useful scripts
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run dev # Vite frontend + local Copilot runtime
|
|
54
|
+
npm run build # Build the frontend and compile the runtime
|
|
55
|
+
npm run preview # Preview the built frontend
|
|
56
|
+
npm run start # Run the compiled runtime from dist-server/
|
|
57
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__APP_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
|
|
8
|
+
"dev:client": "vite",
|
|
9
|
+
"dev:server": "tsx server/index.ts",
|
|
10
|
+
"build": "npm run build:client && npm run build:server",
|
|
11
|
+
"build:client": "vite build",
|
|
12
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"start": "node dist-server/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@askable-ui/react": "^__ASKABLE_VERSION__",
|
|
18
|
+
"@copilotkit/react-core": "__COPILOTKIT_VERSION__",
|
|
19
|
+
"@copilotkit/react-ui": "__COPILOTKIT_VERSION__",
|
|
20
|
+
"react": "19.2.5",
|
|
21
|
+
"react-dom": "19.2.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@copilotkit/runtime": "__COPILOTKIT_VERSION__",
|
|
25
|
+
"@types/cors": "2.8.19",
|
|
26
|
+
"@types/express": "5.0.6",
|
|
27
|
+
"@types/node": "25.6.0",
|
|
28
|
+
"@types/react": "19.2.14",
|
|
29
|
+
"@types/react-dom": "19.2.3",
|
|
30
|
+
"@vitejs/plugin-react": "6.0.1",
|
|
31
|
+
"concurrently": "9.2.1",
|
|
32
|
+
"cors": "2.8.6",
|
|
33
|
+
"dotenv": "17.4.2",
|
|
34
|
+
"express": "5.2.1",
|
|
35
|
+
"openai": "6.34.0",
|
|
36
|
+
"tsx": "4.21.0",
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vite": "8.0.9"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import type { Request, Response } from 'express';
|
|
5
|
+
import { BuiltInAgent, CopilotRuntime, createCopilotExpressHandler } from '@copilotkit/runtime/v2';
|
|
6
|
+
|
|
7
|
+
const port = Number(process.env.PORT ?? 3001);
|
|
8
|
+
const endpoint = '/api/copilotkit';
|
|
9
|
+
const frontendOrigin = process.env.CORS_ORIGIN ?? 'http://localhost:5173';
|
|
10
|
+
const openAiApiKey = process.env.OPENAI_API_KEY;
|
|
11
|
+
const openAiModel = process.env.OPENAI_MODEL ?? 'gpt-4o-mini';
|
|
12
|
+
|
|
13
|
+
const runtime = new CopilotRuntime({
|
|
14
|
+
agents: {
|
|
15
|
+
default: new BuiltInAgent({
|
|
16
|
+
model: `openai/${openAiModel}`,
|
|
17
|
+
apiKey: openAiApiKey,
|
|
18
|
+
prompt:
|
|
19
|
+
'You are a helpful revenue operations copilot. Use the Askable current-focus context and recent focus history to answer precisely about the currently selected KPI, chart, or deal row. If the user has not selected anything yet, ask them what they want to inspect.',
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
app.use(express.json({ limit: '2mb' }));
|
|
26
|
+
app.use(cors({ origin: frontendOrigin, credentials: true }));
|
|
27
|
+
app.use(
|
|
28
|
+
createCopilotExpressHandler({
|
|
29
|
+
runtime,
|
|
30
|
+
basePath: endpoint,
|
|
31
|
+
mode: 'single-route',
|
|
32
|
+
cors: {
|
|
33
|
+
origin: frontendOrigin,
|
|
34
|
+
credentials: true,
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
app.get('/health', (_req: Request, res: Response) => {
|
|
40
|
+
res.json({
|
|
41
|
+
ok: true,
|
|
42
|
+
endpoint,
|
|
43
|
+
llmReady: Boolean(openAiApiKey),
|
|
44
|
+
model: `openai/${openAiModel}`,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
app.listen(port, () => {
|
|
49
|
+
const status = openAiApiKey
|
|
50
|
+
? `BuiltInAgent using openai/${openAiModel}`
|
|
51
|
+
: `BuiltInAgent using openai/${openAiModel} (set OPENAI_API_KEY to enable live responses)`;
|
|
52
|
+
console.log(`Copilot runtime listening on http://localhost:${port}${endpoint}`);
|
|
53
|
+
console.log(`Frontend origin: ${frontendOrigin}`);
|
|
54
|
+
console.log(`Agent: ${status}`);
|
|
55
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { CopilotSidebar } from '@copilotkit/react-ui';
|
|
3
|
+
import { useCopilotReadable } from '@copilotkit/react-core';
|
|
4
|
+
import { useAskable } from '@askable-ui/react';
|
|
5
|
+
|
|
6
|
+
type MetricCard = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
delta: string;
|
|
11
|
+
narrative: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Deal = {
|
|
16
|
+
id: string;
|
|
17
|
+
company: string;
|
|
18
|
+
stage: string;
|
|
19
|
+
value: string;
|
|
20
|
+
owner: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const metrics: MetricCard[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'pipeline-coverage',
|
|
26
|
+
label: 'Pipeline coverage',
|
|
27
|
+
value: '3.9x',
|
|
28
|
+
delta: '+0.4x week over week',
|
|
29
|
+
narrative: 'Coverage exceeds the 3.5x target after enterprise expansion in financial services.',
|
|
30
|
+
detail: 'AI focus summary: pipeline coverage is 3.9x against a 3.5x target, driven by enterprise pipeline growth.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'net-revenue-retention',
|
|
34
|
+
label: 'Net revenue retention',
|
|
35
|
+
value: '118%',
|
|
36
|
+
delta: '+6 pts quarter over quarter',
|
|
37
|
+
narrative: 'Existing accounts expanded after onboarding automation shipped to the top five teams.',
|
|
38
|
+
detail: 'AI focus summary: NRR is 118%, up 6 points quarter over quarter due to expansion in installed accounts.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'support-backlog',
|
|
42
|
+
label: 'Support backlog',
|
|
43
|
+
value: '24 tickets',
|
|
44
|
+
delta: '-31% since last Friday',
|
|
45
|
+
narrative: 'Backlog is falling after the self-serve billing flow and routing updates reduced triage load.',
|
|
46
|
+
detail: 'AI focus summary: support backlog fell to 24 tickets, down 31%, after self-serve billing and routing improvements.',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const deals: Deal[] = [
|
|
51
|
+
{ id: 'acme', company: 'Acme Foods', stage: 'Security review', value: '$84k', owner: 'Nina' },
|
|
52
|
+
{ id: 'northstar', company: 'Northstar Health', stage: 'Champion identified', value: '$122k', owner: 'Sam' },
|
|
53
|
+
{ id: 'lattice', company: 'Lattice Cloud', stage: 'Commercials', value: '$61k', owner: 'Aria' },
|
|
54
|
+
{ id: 'meridian', company: 'Meridian Retail', stage: 'Pilot live', value: '$48k', owner: 'Jon' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function askableMeta(meta: Record<string, string>) {
|
|
58
|
+
return JSON.stringify(meta);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function App() {
|
|
62
|
+
const { ctx, promptContext } = useAskable();
|
|
63
|
+
const [selectedPanel, setSelectedPanel] = useState<string | null>(null);
|
|
64
|
+
const panelRefs = useRef<Record<string, HTMLElement | null>>({});
|
|
65
|
+
|
|
66
|
+
const currentContext = promptContext || 'No panel selected yet. Hover or click a KPI card or deal row to feed Askable context into CopilotKit.';
|
|
67
|
+
const recentContext = useMemo(() => {
|
|
68
|
+
const history = ctx.toHistoryContext(4);
|
|
69
|
+
return history || 'Recent UI history will appear here after a few interactions.';
|
|
70
|
+
}, [ctx, promptContext]);
|
|
71
|
+
|
|
72
|
+
useCopilotReadable(
|
|
73
|
+
{
|
|
74
|
+
description: 'The Askable UI panel the user is currently focused on.',
|
|
75
|
+
value: currentContext,
|
|
76
|
+
},
|
|
77
|
+
[currentContext],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
useCopilotReadable(
|
|
81
|
+
{
|
|
82
|
+
description: 'Recent Askable UI focus history from newest to oldest.',
|
|
83
|
+
value: recentContext,
|
|
84
|
+
},
|
|
85
|
+
[recentContext],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
function focusPanel(id: string) {
|
|
89
|
+
const panel = panelRefs.current[id];
|
|
90
|
+
if (!panel) return;
|
|
91
|
+
ctx.select(panel);
|
|
92
|
+
setSelectedPanel(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="app-shell">
|
|
97
|
+
<main className="workspace">
|
|
98
|
+
<section className="hero card">
|
|
99
|
+
<div>
|
|
100
|
+
<p className="eyebrow">askable-ui + CopilotKit starter</p>
|
|
101
|
+
<h1>Ship a UI-aware copilot in one scaffold.</h1>
|
|
102
|
+
<p className="hero-copy">
|
|
103
|
+
Hover or click any panel to update Askable context. CopilotKit receives the current focus and recent history via
|
|
104
|
+
<code> useCopilotReadable() </code>
|
|
105
|
+
so the assistant can answer about exactly what the user sees.
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="hero-actions">
|
|
109
|
+
<button type="button" onClick={() => focusPanel('pipeline-coverage')}>
|
|
110
|
+
Ask about pipeline
|
|
111
|
+
</button>
|
|
112
|
+
<button type="button" className="secondary" onClick={() => focusPanel('northstar')}>
|
|
113
|
+
Ask about Northstar
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
|
|
118
|
+
<section className="metrics-grid">
|
|
119
|
+
{metrics.map((metric) => (
|
|
120
|
+
<article
|
|
121
|
+
key={metric.id}
|
|
122
|
+
ref={(node) => {
|
|
123
|
+
panelRefs.current[metric.id] = node;
|
|
124
|
+
}}
|
|
125
|
+
className={`card metric-card ${selectedPanel === metric.id ? 'is-selected' : ''}`}
|
|
126
|
+
data-askable={askableMeta({
|
|
127
|
+
area: 'executive dashboard',
|
|
128
|
+
panel: metric.label,
|
|
129
|
+
value: metric.value,
|
|
130
|
+
delta: metric.delta,
|
|
131
|
+
})}
|
|
132
|
+
data-askable-text={metric.detail}
|
|
133
|
+
>
|
|
134
|
+
<div className="metric-head">
|
|
135
|
+
<span>{metric.label}</span>
|
|
136
|
+
<span className="delta">{metric.delta}</span>
|
|
137
|
+
</div>
|
|
138
|
+
<strong>{metric.value}</strong>
|
|
139
|
+
<p>{metric.narrative}</p>
|
|
140
|
+
<button type="button" className="link-button" onClick={() => focusPanel(metric.id)}>
|
|
141
|
+
Ask AI about this card
|
|
142
|
+
</button>
|
|
143
|
+
</article>
|
|
144
|
+
))}
|
|
145
|
+
</section>
|
|
146
|
+
|
|
147
|
+
<section className="grid-two">
|
|
148
|
+
<article
|
|
149
|
+
className={`card chart-card ${selectedPanel === 'forecast' ? 'is-selected' : ''}`}
|
|
150
|
+
ref={(node) => {
|
|
151
|
+
panelRefs.current.forecast = node;
|
|
152
|
+
}}
|
|
153
|
+
data-askable={askableMeta({
|
|
154
|
+
area: 'forecast',
|
|
155
|
+
chart: 'quarterly revenue',
|
|
156
|
+
trend: 'up and to the right',
|
|
157
|
+
confidence: 'medium-high',
|
|
158
|
+
})}
|
|
159
|
+
data-askable-text="AI focus summary: quarterly revenue is rising steadily from $1.2M in Q1 to a projected $1.9M in Q4, with strongest momentum in enterprise expansions."
|
|
160
|
+
>
|
|
161
|
+
<div className="section-head">
|
|
162
|
+
<div>
|
|
163
|
+
<p className="eyebrow">Forecast</p>
|
|
164
|
+
<h2>Quarterly revenue outlook</h2>
|
|
165
|
+
</div>
|
|
166
|
+
<button type="button" className="link-button" onClick={() => focusPanel('forecast')}>
|
|
167
|
+
Ask AI about this chart
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="bars" aria-hidden="true">
|
|
171
|
+
<span style={{ height: '46%' }} />
|
|
172
|
+
<span style={{ height: '59%' }} />
|
|
173
|
+
<span style={{ height: '74%' }} />
|
|
174
|
+
<span style={{ height: '92%' }} />
|
|
175
|
+
</div>
|
|
176
|
+
<p className="chart-caption">Q4 upside comes from larger multi-product expansions rather than raw logo growth.</p>
|
|
177
|
+
</article>
|
|
178
|
+
|
|
179
|
+
<article className="card context-card">
|
|
180
|
+
<div className="section-head compact">
|
|
181
|
+
<div>
|
|
182
|
+
<p className="eyebrow">Live Askable context</p>
|
|
183
|
+
<h2>What CopilotKit can currently read</h2>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<pre>{currentContext}</pre>
|
|
187
|
+
<h3>Recent focus history</h3>
|
|
188
|
+
<pre>{recentContext}</pre>
|
|
189
|
+
</article>
|
|
190
|
+
</section>
|
|
191
|
+
|
|
192
|
+
<section className="card deals-card">
|
|
193
|
+
<div className="section-head">
|
|
194
|
+
<div>
|
|
195
|
+
<p className="eyebrow">Pipeline drilldown</p>
|
|
196
|
+
<h2>Priority deals</h2>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="table-head">
|
|
200
|
+
<span>Account</span>
|
|
201
|
+
<span>Stage</span>
|
|
202
|
+
<span>Value</span>
|
|
203
|
+
<span>Owner</span>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="deal-list">
|
|
206
|
+
{deals.map((deal) => (
|
|
207
|
+
<button
|
|
208
|
+
key={deal.id}
|
|
209
|
+
type="button"
|
|
210
|
+
ref={(node) => {
|
|
211
|
+
panelRefs.current[deal.id] = node;
|
|
212
|
+
}}
|
|
213
|
+
className={`deal-row ${selectedPanel === deal.id ? 'is-selected' : ''}`}
|
|
214
|
+
data-askable={askableMeta({
|
|
215
|
+
area: 'pipeline drilldown',
|
|
216
|
+
account: deal.company,
|
|
217
|
+
stage: deal.stage,
|
|
218
|
+
value: deal.value,
|
|
219
|
+
owner: deal.owner,
|
|
220
|
+
})}
|
|
221
|
+
data-askable-text={`AI focus summary: ${deal.company} is in ${deal.stage}, worth ${deal.value}, owned by ${deal.owner}.`}
|
|
222
|
+
onClick={() => focusPanel(deal.id)}
|
|
223
|
+
>
|
|
224
|
+
<span>{deal.company}</span>
|
|
225
|
+
<span>{deal.stage}</span>
|
|
226
|
+
<span>{deal.value}</span>
|
|
227
|
+
<span>{deal.owner}</span>
|
|
228
|
+
</button>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</section>
|
|
232
|
+
</main>
|
|
233
|
+
|
|
234
|
+
<aside className="copilot-panel">
|
|
235
|
+
<div className="copilot-panel-header">
|
|
236
|
+
<p className="eyebrow">CopilotKit sidebar</p>
|
|
237
|
+
<h2>Ask about whatever is selected</h2>
|
|
238
|
+
<p>
|
|
239
|
+
Add <code>OPENAI_API_KEY</code> to <code>.env</code> to enable responses from the local runtime.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
<CopilotSidebar
|
|
243
|
+
instructions="You are a helpful revenue operations copilot. Use the Askable context and recent focus history to answer precisely about the currently selected panel. If no panel is selected, ask the user what they want to inspect."
|
|
244
|
+
defaultOpen
|
|
245
|
+
clickOutsideToClose={false}
|
|
246
|
+
labels={{
|
|
247
|
+
title: 'Askable Copilot',
|
|
248
|
+
initial: 'Ask about the highlighted KPI, chart, or deal.',
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</aside>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { CopilotKit } from '@copilotkit/react-core';
|
|
4
|
+
import '@copilotkit/react-ui/styles.css';
|
|
5
|
+
import './styles.css';
|
|
6
|
+
import App from './App';
|
|
7
|
+
|
|
8
|
+
const runtimeUrl = import.meta.env.VITE_COPILOT_RUNTIME_URL ?? 'http://localhost:3001/api/copilotkit';
|
|
9
|
+
|
|
10
|
+
createRoot(document.getElementById('root')!).render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<CopilotKit runtimeUrl={runtimeUrl}>
|
|
13
|
+
<App />
|
|
14
|
+
</CopilotKit>
|
|
15
|
+
</StrictMode>,
|
|
16
|
+
);
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4
|
+
background: #07111f;
|
|
5
|
+
color: #e5eefb;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
background:
|
|
16
|
+
radial-gradient(circle at top left, rgba(56, 189, 248, 0.18), transparent 24%),
|
|
17
|
+
radial-gradient(circle at top right, rgba(14, 165, 233, 0.15), transparent 18%),
|
|
18
|
+
linear-gradient(180deg, #07111f 0%, #0b1729 100%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
a {
|
|
22
|
+
color: inherit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button,
|
|
26
|
+
input,
|
|
27
|
+
textarea {
|
|
28
|
+
font: inherit;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
code,
|
|
32
|
+
pre {
|
|
33
|
+
font-family: 'SFMono-Regular', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#root {
|
|
37
|
+
min-height: 100vh;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.app-shell {
|
|
41
|
+
display: grid;
|
|
42
|
+
grid-template-columns: minmax(0, 1fr) 420px;
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.workspace {
|
|
47
|
+
padding: 32px;
|
|
48
|
+
display: grid;
|
|
49
|
+
gap: 24px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.card {
|
|
53
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
54
|
+
background: rgba(10, 17, 31, 0.78);
|
|
55
|
+
backdrop-filter: blur(14px);
|
|
56
|
+
border-radius: 24px;
|
|
57
|
+
padding: 24px;
|
|
58
|
+
box-shadow: 0 18px 48px rgba(2, 8, 23, 0.28);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.eyebrow {
|
|
62
|
+
text-transform: uppercase;
|
|
63
|
+
letter-spacing: 0.14em;
|
|
64
|
+
font-size: 0.72rem;
|
|
65
|
+
color: #7dd3fc;
|
|
66
|
+
margin: 0 0 12px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.hero {
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
gap: 24px;
|
|
73
|
+
align-items: flex-end;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.hero h1,
|
|
77
|
+
.section-head h2,
|
|
78
|
+
.context-card h2,
|
|
79
|
+
.copilot-panel-header h2 {
|
|
80
|
+
margin: 0;
|
|
81
|
+
font-size: clamp(1.6rem, 3vw, 2.8rem);
|
|
82
|
+
line-height: 1.05;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hero-copy,
|
|
86
|
+
.copilot-panel-header p,
|
|
87
|
+
.metric-card p,
|
|
88
|
+
.chart-caption {
|
|
89
|
+
color: #b7c6dc;
|
|
90
|
+
line-height: 1.65;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hero-actions {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: 12px;
|
|
96
|
+
flex-wrap: wrap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
button {
|
|
100
|
+
border: 0;
|
|
101
|
+
border-radius: 999px;
|
|
102
|
+
padding: 0.82rem 1.1rem;
|
|
103
|
+
background: linear-gradient(135deg, #38bdf8, #2563eb);
|
|
104
|
+
color: #eff6ff;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: transform 140ms ease, box-shadow 140ms ease, opacity 140ms ease;
|
|
107
|
+
box-shadow: 0 12px 32px rgba(37, 99, 235, 0.28);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
button:hover {
|
|
111
|
+
transform: translateY(-1px);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
button.secondary,
|
|
115
|
+
.link-button {
|
|
116
|
+
background: rgba(30, 41, 59, 0.85);
|
|
117
|
+
color: #d8e7fb;
|
|
118
|
+
box-shadow: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.link-button {
|
|
122
|
+
padding-left: 0;
|
|
123
|
+
padding-right: 0;
|
|
124
|
+
background: transparent;
|
|
125
|
+
color: #7dd3fc;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metrics-grid {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
131
|
+
gap: 20px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.metric-card strong {
|
|
135
|
+
display: block;
|
|
136
|
+
font-size: 2rem;
|
|
137
|
+
margin-bottom: 12px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.metric-head,
|
|
141
|
+
.section-head,
|
|
142
|
+
.table-head,
|
|
143
|
+
.deal-row {
|
|
144
|
+
display: grid;
|
|
145
|
+
align-items: center;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.metric-head {
|
|
149
|
+
grid-template-columns: 1fr auto;
|
|
150
|
+
gap: 12px;
|
|
151
|
+
color: #d8e7fb;
|
|
152
|
+
margin-bottom: 18px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.delta {
|
|
156
|
+
color: #7dd3fc;
|
|
157
|
+
font-size: 0.9rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.grid-two {
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
|
163
|
+
gap: 20px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.section-head {
|
|
167
|
+
grid-template-columns: 1fr auto;
|
|
168
|
+
gap: 16px;
|
|
169
|
+
margin-bottom: 18px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.section-head.compact {
|
|
173
|
+
grid-template-columns: 1fr;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.bars {
|
|
177
|
+
height: 220px;
|
|
178
|
+
display: grid;
|
|
179
|
+
grid-template-columns: repeat(4, 1fr);
|
|
180
|
+
align-items: end;
|
|
181
|
+
gap: 18px;
|
|
182
|
+
padding: 12px 0 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.bars span {
|
|
186
|
+
border-radius: 20px 20px 10px 10px;
|
|
187
|
+
background: linear-gradient(180deg, #38bdf8 0%, #1d4ed8 100%);
|
|
188
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.context-card pre {
|
|
192
|
+
margin: 0;
|
|
193
|
+
white-space: pre-wrap;
|
|
194
|
+
overflow-wrap: anywhere;
|
|
195
|
+
background: rgba(15, 23, 42, 0.9);
|
|
196
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
197
|
+
border-radius: 18px;
|
|
198
|
+
padding: 14px;
|
|
199
|
+
color: #d9e7f8;
|
|
200
|
+
line-height: 1.55;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.context-card h3 {
|
|
204
|
+
margin: 18px 0 10px;
|
|
205
|
+
font-size: 0.95rem;
|
|
206
|
+
color: #8acdf3;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.table-head,
|
|
210
|
+
.deal-row {
|
|
211
|
+
grid-template-columns: 1.2fr 1fr 0.7fr 0.7fr;
|
|
212
|
+
gap: 12px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.table-head {
|
|
216
|
+
color: #8da2c0;
|
|
217
|
+
font-size: 0.84rem;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
letter-spacing: 0.12em;
|
|
220
|
+
padding: 0 0 10px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.deal-list {
|
|
224
|
+
display: grid;
|
|
225
|
+
gap: 12px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.deal-row {
|
|
229
|
+
width: 100%;
|
|
230
|
+
text-align: left;
|
|
231
|
+
padding: 16px 18px;
|
|
232
|
+
border-radius: 18px;
|
|
233
|
+
background: rgba(15, 23, 42, 0.92);
|
|
234
|
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
235
|
+
color: #e2ebf8;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.metric-card.is-selected,
|
|
239
|
+
.chart-card.is-selected,
|
|
240
|
+
.deal-row.is-selected {
|
|
241
|
+
border-color: rgba(56, 189, 248, 0.9);
|
|
242
|
+
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.25), 0 18px 48px rgba(2, 8, 23, 0.28);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.copilot-panel {
|
|
246
|
+
border-left: 1px solid rgba(148, 163, 184, 0.18);
|
|
247
|
+
background: rgba(2, 6, 23, 0.75);
|
|
248
|
+
padding: 24px;
|
|
249
|
+
display: grid;
|
|
250
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
251
|
+
gap: 18px;
|
|
252
|
+
min-height: 100vh;
|
|
253
|
+
position: sticky;
|
|
254
|
+
top: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.copilot-panel-header {
|
|
258
|
+
padding: 12px 4px 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.copilot-panel :where(.copilotKitWindow, .copilotKitSidebar, .copilotKitChat) {
|
|
262
|
+
height: 100%;
|
|
263
|
+
max-height: calc(100vh - 160px);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@media (max-width: 1180px) {
|
|
267
|
+
.app-shell {
|
|
268
|
+
grid-template-columns: 1fr;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.copilot-panel {
|
|
272
|
+
position: static;
|
|
273
|
+
min-height: auto;
|
|
274
|
+
border-left: 0;
|
|
275
|
+
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@media (max-width: 920px) {
|
|
280
|
+
.metrics-grid,
|
|
281
|
+
.grid-two {
|
|
282
|
+
grid-template-columns: 1fr;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.hero {
|
|
286
|
+
flex-direction: column;
|
|
287
|
+
align-items: flex-start;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@media (max-width: 720px) {
|
|
292
|
+
.workspace,
|
|
293
|
+
.copilot-panel {
|
|
294
|
+
padding: 18px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.table-head {
|
|
298
|
+
display: none;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.deal-row {
|
|
302
|
+
grid-template-columns: 1fr 1fr;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
6
|
+
"allowJs": false,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"moduleResolution": "Bundler",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx"
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"],
|
|
20
|
+
"references": [{ "path": "./tsconfig.server.json" }]
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"rootDir": "server",
|
|
8
|
+
"outDir": "dist-server",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["server/**/*.ts"]
|
|
16
|
+
}
|