@yudin-s/react-chrome-ai 0.1.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/SECURITY.md +12 -0
  5. package/dist/index.cjs +938 -0
  6. package/dist/index.d.cts +431 -0
  7. package/dist/index.d.ts +431 -0
  8. package/dist/index.js +868 -0
  9. package/docs/ai-agents.md +155 -0
  10. package/docs/hooks.md +378 -0
  11. package/docs/publishing.md +102 -0
  12. package/examples/README.md +24 -0
  13. package/examples/basic-prompt.tsx +18 -0
  14. package/examples/chrome-ai-studio/README.md +22 -0
  15. package/examples/chrome-ai-studio/index.html +12 -0
  16. package/examples/chrome-ai-studio/package.json +22 -0
  17. package/examples/chrome-ai-studio/src/App.tsx +138 -0
  18. package/examples/chrome-ai-studio/src/main.tsx +10 -0
  19. package/examples/chrome-ai-studio/src/styles.css +219 -0
  20. package/examples/chrome-ai-studio/tsconfig.json +16 -0
  21. package/examples/chrome-ai-studio/vite.config.ts +6 -0
  22. package/examples/local-review-workbench/README.md +18 -0
  23. package/examples/local-review-workbench/index.html +12 -0
  24. package/examples/local-review-workbench/package.json +22 -0
  25. package/examples/local-review-workbench/src/App.tsx +121 -0
  26. package/examples/local-review-workbench/src/main.tsx +10 -0
  27. package/examples/local-review-workbench/src/styles.css +190 -0
  28. package/examples/local-review-workbench/tsconfig.json +16 -0
  29. package/examples/local-review-workbench/vite.config.ts +6 -0
  30. package/examples/local-translator/README.md +24 -0
  31. package/examples/local-translator/index.html +12 -0
  32. package/examples/local-translator/package.json +23 -0
  33. package/examples/local-translator/src/App.tsx +235 -0
  34. package/examples/local-translator/src/main.tsx +10 -0
  35. package/examples/local-translator/src/styles.css +161 -0
  36. package/examples/local-translator/tsconfig.json +16 -0
  37. package/examples/local-translator/vite.config.ts +6 -0
  38. package/examples/model-download-progress.tsx +20 -0
  39. package/examples/summarizer.tsx +28 -0
  40. package/llms.txt +97 -0
  41. package/package.json +86 -0
@@ -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>Chrome AI Studio</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,22 @@
1
+ {
2
+ "name": "react-chrome-ai-studio-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc --noEmit && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@vitejs/plugin-react": "^4.3.4",
12
+ "@yudin-s/react-chrome-ai": "file:../..",
13
+ "vite": "^6.0.7",
14
+ "typescript": "^5.7.2",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.2"
21
+ }
22
+ }
@@ -0,0 +1,138 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ useChromeAIAvailability,
4
+ useChromeAIContext,
5
+ useChromeAIParams,
6
+ useChromeAISession,
7
+ useChromeAIStream,
8
+ } from "@yudin-s/react-chrome-ai";
9
+
10
+ const prompts = [
11
+ "Explain what Chrome Built-in AI changes for privacy-sensitive React apps.",
12
+ "Draft a compact onboarding checklist for enabling local model features.",
13
+ "List three UX states a browser-native AI app must handle.",
14
+ ];
15
+
16
+ export function App() {
17
+ const [prompt, setPrompt] = useState(prompts[0]);
18
+ const readiness = useChromeAIAvailability();
19
+ const params = useChromeAIParams();
20
+ const model = useChromeAISession({
21
+ autoCreate: false,
22
+ createOptions: {
23
+ initialPrompts: [
24
+ {
25
+ role: "system",
26
+ content:
27
+ "You are a concise local assistant. Be practical, specific, and avoid unsupported claims.",
28
+ },
29
+ ],
30
+ expectedInputs: [{ type: "text", languages: ["en"] }],
31
+ expectedOutputs: [{ type: "text", languages: ["en"] }],
32
+ },
33
+ });
34
+ const stream = useChromeAIStream(model.session);
35
+ const context = useChromeAIContext(model.session, { pollIntervalMs: 1000 });
36
+
37
+ const contextPercent = useMemo(() => {
38
+ if (!context.contextWindow || !context.contextUsage) return 0;
39
+ return Math.min(100, Math.round((context.contextUsage / context.contextWindow) * 100));
40
+ }, [context.contextUsage, context.contextWindow]);
41
+
42
+ return (
43
+ <main className="shell">
44
+ <header className="topbar">
45
+ <div>
46
+ <p className="eyebrow">React Chrome AI example</p>
47
+ <h1>Chrome AI Studio</h1>
48
+ </div>
49
+ <a href="https://developer.chrome.com/docs/ai/prompt-api">Prompt API docs</a>
50
+ </header>
51
+
52
+ <section className="status-grid">
53
+ <StatusTile label="API support" value={readiness.supported ? "Supported" : "Not exposed"} />
54
+ <StatusTile label="Availability" value={readiness.availability ?? readiness.status} />
55
+ <StatusTile label="Session" value={model.status} />
56
+ <StatusTile label="User activation" value={readiness.userActivation ? "Present" : "Needed"} />
57
+ </section>
58
+
59
+ <section className="workspace">
60
+ <aside className="panel">
61
+ <h2>Model Control</h2>
62
+ <p className="muted">
63
+ Prepare the model from a click. Chrome may use this step to download or load browser-managed model files.
64
+ </p>
65
+ <button onClick={() => model.createSession()} disabled={model.status === "checking" || model.status === "downloading"}>
66
+ Prepare Model
67
+ </button>
68
+ <button className="secondary" onClick={model.destroySession} disabled={!model.session}>
69
+ Destroy Session
70
+ </button>
71
+
72
+ <div className="progress-block">
73
+ <div>
74
+ <strong>Download</strong>
75
+ <span>{model.progress?.percent != null ? `${model.progress.percent}%` : model.status}</span>
76
+ </div>
77
+ {model.progress?.progress != null ? (
78
+ <progress value={model.progress.progress} max={1} />
79
+ ) : (
80
+ <progress />
81
+ )}
82
+ </div>
83
+
84
+ <div className="meter">
85
+ <div>
86
+ <strong>Context</strong>
87
+ <span>{contextPercent}%</span>
88
+ </div>
89
+ <meter value={context.contextUsage ?? 0} max={context.contextWindow ?? 1} />
90
+ {context.overflowed && <p role="alert">Context overflow was reported by Chrome.</p>}
91
+ </div>
92
+
93
+ <dl>
94
+ <dt>Top K</dt>
95
+ <dd>{params.params?.defaultTopK ?? "unknown"}</dd>
96
+ <dt>Temperature</dt>
97
+ <dd>{params.params?.defaultTemperature ?? "unknown"}</dd>
98
+ </dl>
99
+ </aside>
100
+
101
+ <section className="panel editor">
102
+ <h2>Streaming Prompt</h2>
103
+ <label>
104
+ Prompt
105
+ <textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} rows={8} />
106
+ </label>
107
+ <div className="preset-row">
108
+ {prompts.map((item) => (
109
+ <button className="chip" key={item} onClick={() => setPrompt(item)}>
110
+ {item.slice(0, 34)}
111
+ </button>
112
+ ))}
113
+ </div>
114
+ <button disabled={!model.session || stream.status === "streaming"} onClick={() => stream.streamPrompt(prompt)}>
115
+ Stream Response
116
+ </button>
117
+ {model.error && <p role="alert">{model.error.message}</p>}
118
+ {stream.error && <p role="alert">{stream.error.message}</p>}
119
+ </section>
120
+
121
+ <section className="panel output">
122
+ <h2>Output</h2>
123
+ <p className="muted">Status: {stream.status}</p>
124
+ <output>{stream.text || "The local model response will appear here."}</output>
125
+ </section>
126
+ </section>
127
+ </main>
128
+ );
129
+ }
130
+
131
+ function StatusTile({ label, value }: { label: string; value: string }) {
132
+ return (
133
+ <div className="status-tile">
134
+ <span>{label}</span>
135
+ <strong>{value}</strong>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./styles.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -0,0 +1,219 @@
1
+ :root {
2
+ color: #17202a;
3
+ background: #f6f8fb;
4
+ font-family:
5
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
6
+ sans-serif;
7
+ }
8
+
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ }
16
+
17
+ button,
18
+ textarea {
19
+ font: inherit;
20
+ }
21
+
22
+ button {
23
+ min-height: 40px;
24
+ border: 0;
25
+ border-radius: 6px;
26
+ padding: 0 14px;
27
+ background: #1769aa;
28
+ color: white;
29
+ cursor: pointer;
30
+ }
31
+
32
+ button:disabled {
33
+ cursor: not-allowed;
34
+ opacity: 0.55;
35
+ }
36
+
37
+ button.secondary {
38
+ background: #334155;
39
+ }
40
+
41
+ button.chip {
42
+ min-height: 32px;
43
+ background: #e7eef7;
44
+ color: #17202a;
45
+ }
46
+
47
+ .shell {
48
+ width: min(1180px, calc(100% - 32px));
49
+ margin: 0 auto;
50
+ padding: 32px 0 48px;
51
+ }
52
+
53
+ .topbar {
54
+ display: flex;
55
+ align-items: end;
56
+ justify-content: space-between;
57
+ gap: 24px;
58
+ margin-bottom: 24px;
59
+ }
60
+
61
+ .topbar a {
62
+ color: #1769aa;
63
+ font-weight: 700;
64
+ }
65
+
66
+ .eyebrow {
67
+ margin: 0 0 6px;
68
+ color: #607089;
69
+ font-size: 0.82rem;
70
+ font-weight: 800;
71
+ text-transform: uppercase;
72
+ }
73
+
74
+ h1,
75
+ h2,
76
+ p {
77
+ margin-top: 0;
78
+ }
79
+
80
+ h1 {
81
+ margin-bottom: 0;
82
+ font-size: clamp(2.2rem, 5vw, 4.6rem);
83
+ line-height: 0.95;
84
+ }
85
+
86
+ h2 {
87
+ font-size: 1.05rem;
88
+ }
89
+
90
+ .status-grid {
91
+ display: grid;
92
+ grid-template-columns: repeat(4, minmax(0, 1fr));
93
+ gap: 12px;
94
+ margin-bottom: 18px;
95
+ }
96
+
97
+ .status-tile,
98
+ .panel {
99
+ border: 1px solid #d9e1ea;
100
+ border-radius: 8px;
101
+ background: white;
102
+ box-shadow: 0 10px 24px rgba(23, 32, 42, 0.06);
103
+ }
104
+
105
+ .status-tile {
106
+ padding: 14px;
107
+ }
108
+
109
+ .status-tile span,
110
+ .muted,
111
+ dt {
112
+ color: #65758a;
113
+ }
114
+
115
+ .status-tile strong {
116
+ display: block;
117
+ margin-top: 6px;
118
+ overflow-wrap: anywhere;
119
+ }
120
+
121
+ .workspace {
122
+ display: grid;
123
+ grid-template-columns: 300px 1fr 1fr;
124
+ gap: 18px;
125
+ align-items: stretch;
126
+ }
127
+
128
+ .panel {
129
+ padding: 18px;
130
+ }
131
+
132
+ .panel > button + button {
133
+ margin-left: 8px;
134
+ }
135
+
136
+ .progress-block,
137
+ .meter,
138
+ dl {
139
+ margin-top: 18px;
140
+ border-top: 1px solid #e5ebf2;
141
+ padding-top: 16px;
142
+ }
143
+
144
+ .progress-block div,
145
+ .meter div {
146
+ display: flex;
147
+ justify-content: space-between;
148
+ gap: 12px;
149
+ }
150
+
151
+ progress,
152
+ meter {
153
+ width: 100%;
154
+ height: 14px;
155
+ margin-top: 8px;
156
+ }
157
+
158
+ dl {
159
+ display: grid;
160
+ grid-template-columns: 1fr auto;
161
+ gap: 8px 12px;
162
+ }
163
+
164
+ dd {
165
+ margin: 0;
166
+ font-weight: 700;
167
+ }
168
+
169
+ label {
170
+ display: grid;
171
+ gap: 8px;
172
+ color: #3b4a5f;
173
+ font-weight: 700;
174
+ }
175
+
176
+ textarea {
177
+ width: 100%;
178
+ min-height: 190px;
179
+ resize: vertical;
180
+ border: 1px solid #c9d4df;
181
+ border-radius: 6px;
182
+ padding: 12px;
183
+ color: #17202a;
184
+ }
185
+
186
+ .preset-row {
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ gap: 8px;
190
+ margin: 12px 0;
191
+ }
192
+
193
+ output {
194
+ display: block;
195
+ min-height: 320px;
196
+ white-space: pre-wrap;
197
+ border: 1px solid #d9e1ea;
198
+ border-radius: 6px;
199
+ padding: 14px;
200
+ background: #f8fafc;
201
+ line-height: 1.55;
202
+ }
203
+
204
+ [role="alert"] {
205
+ color: #b42318;
206
+ font-weight: 700;
207
+ }
208
+
209
+ @media (max-width: 980px) {
210
+ .status-grid,
211
+ .workspace {
212
+ grid-template-columns: 1fr;
213
+ }
214
+
215
+ .topbar {
216
+ align-items: start;
217
+ flex-direction: column;
218
+ }
219
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "lib": ["DOM", "DOM.Iterable", "ES2021"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "isolatedModules": true
14
+ },
15
+ "include": ["src", "vite.config.ts"]
16
+ }
@@ -0,0 +1,6 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });
@@ -0,0 +1,18 @@
1
+ # Local Review Workbench Example
2
+
3
+ React site showing how to combine `LanguageModel` structured output with Chrome Built-in task APIs.
4
+
5
+ It demonstrates:
6
+
7
+ - `useChromeAIPrompt` with JSON schema-style `responseConstraint`;
8
+ - reflection pass for output repair;
9
+ - progress/error state;
10
+ - Summarizer task API as a companion tool;
11
+ - a realistic review workflow rather than a bare prompt button.
12
+
13
+ ## Run
14
+
15
+ ```bash
16
+ npm install
17
+ npm run dev
18
+ ```
@@ -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>Local Review Workbench</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,22 @@
1
+ {
2
+ "name": "react-chrome-ai-local-review-workbench-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc --noEmit && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@vitejs/plugin-react": "^4.3.4",
12
+ "@yudin-s/react-chrome-ai": "file:../..",
13
+ "vite": "^6.0.7",
14
+ "typescript": "^5.7.2",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.2"
21
+ }
22
+ }
@@ -0,0 +1,121 @@
1
+ import { useMemo, useState } from "react";
2
+ import { useChromeAIPrompt, useChromeAISummarizer } from "@yudin-s/react-chrome-ai";
3
+
4
+ type ReviewResult = {
5
+ summary: string;
6
+ risk: "low" | "medium" | "high";
7
+ findings: Array<{ title: string; severity: "low" | "medium" | "high"; detail: string }>;
8
+ };
9
+
10
+ const sampleDiff = `diff --git a/src/auth.ts b/src/auth.ts
11
+ +export async function login(password: string) {
12
+ + const token = localStorage.getItem("admin-token");
13
+ + if (password.length > 4 && token) return { ok: true, token };
14
+ + return { ok: false };
15
+ +}
16
+ `;
17
+
18
+ export function App() {
19
+ const [diff, setDiff] = useState(sampleDiff);
20
+ const review = useChromeAIPrompt<ReviewResult>({
21
+ reflection: {
22
+ format: "json",
23
+ reflect: true,
24
+ schema: {
25
+ type: "object",
26
+ properties: {
27
+ summary: { type: "string" },
28
+ risk: { enum: ["low", "medium", "high"] },
29
+ findings: {
30
+ type: "array",
31
+ items: {
32
+ type: "object",
33
+ properties: {
34
+ title: { type: "string" },
35
+ severity: { enum: ["low", "medium", "high"] },
36
+ detail: { type: "string" },
37
+ },
38
+ required: ["title", "severity", "detail"],
39
+ },
40
+ },
41
+ },
42
+ required: ["summary", "risk", "findings"],
43
+ },
44
+ },
45
+ });
46
+ const summarizer = useChromeAISummarizer({
47
+ createOptions: {
48
+ type: "key-points",
49
+ format: "markdown",
50
+ length: "short",
51
+ expectedInputLanguages: ["en"],
52
+ outputLanguage: "en",
53
+ },
54
+ });
55
+
56
+ const prompt = useMemo(
57
+ () =>
58
+ `Review this code diff. Return JSON with summary, risk, and findings.\n\n${diff}`,
59
+ [diff]
60
+ );
61
+
62
+ return (
63
+ <main className="shell">
64
+ <header>
65
+ <p>React Chrome AI example</p>
66
+ <h1>Local Review Workbench</h1>
67
+ </header>
68
+
69
+ <section className="layout">
70
+ <section className="panel input-panel">
71
+ <div className="panel-heading">
72
+ <h2>Diff Input</h2>
73
+ <button onClick={() => setDiff(sampleDiff)}>Reset sample</button>
74
+ </div>
75
+ <textarea value={diff} onChange={(event) => setDiff(event.target.value)} />
76
+ <div className="actions">
77
+ <button disabled={review.status === "prompting"} onClick={() => review.promptStructured(prompt)}>
78
+ Structured Review
79
+ </button>
80
+ <button
81
+ className="secondary"
82
+ disabled={summarizer.status === "prompting"}
83
+ onClick={() => summarizer.run(diff)}
84
+ >
85
+ Summarize Diff
86
+ </button>
87
+ </div>
88
+ {(review.progress || summarizer.progress) && (
89
+ <progress value={(review.progress ?? summarizer.progress)?.progress} max={1} />
90
+ )}
91
+ </section>
92
+
93
+ <section className="panel result-panel">
94
+ <h2>Structured Output</h2>
95
+ <div className="risk-strip" data-risk={review.data?.risk ?? "none"}>
96
+ <span>Risk</span>
97
+ <strong>{review.data?.risk ?? "pending"}</strong>
98
+ </div>
99
+ <p>{review.data?.summary ?? "Run a structured review to get local JSON output."}</p>
100
+ <div className="findings">
101
+ {(review.data?.findings ?? []).map((finding) => (
102
+ <article key={finding.title}>
103
+ <span>{finding.severity}</span>
104
+ <h3>{finding.title}</h3>
105
+ <p>{finding.detail}</p>
106
+ </article>
107
+ ))}
108
+ </div>
109
+ {review.error && <p role="alert">{review.error.message}</p>}
110
+ </section>
111
+
112
+ <section className="panel summary-panel">
113
+ <h2>Task API Summary</h2>
114
+ <p className="muted">Status: {summarizer.status}</p>
115
+ <output>{summarizer.text || "Summarizer output appears here."}</output>
116
+ {summarizer.error && <p role="alert">{summarizer.error.message}</p>}
117
+ </section>
118
+ </section>
119
+ </main>
120
+ );
121
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./styles.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );