@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,190 @@
1
+ :root {
2
+ background: #f5f7fa;
3
+ color: #182230;
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: 38px;
24
+ border: 0;
25
+ border-radius: 6px;
26
+ padding: 0 14px;
27
+ background: #0b6b58;
28
+ color: white;
29
+ font-weight: 700;
30
+ }
31
+
32
+ button.secondary {
33
+ background: #475467;
34
+ }
35
+
36
+ button:disabled {
37
+ opacity: 0.55;
38
+ }
39
+
40
+ .shell {
41
+ width: min(1200px, calc(100% - 32px));
42
+ margin: 0 auto;
43
+ padding: 30px 0 48px;
44
+ }
45
+
46
+ header {
47
+ display: flex;
48
+ align-items: end;
49
+ justify-content: space-between;
50
+ gap: 20px;
51
+ margin-bottom: 22px;
52
+ }
53
+
54
+ header p {
55
+ margin: 0;
56
+ color: #667085;
57
+ font-weight: 800;
58
+ text-transform: uppercase;
59
+ }
60
+
61
+ h1 {
62
+ margin: 0;
63
+ font-size: clamp(2.1rem, 4vw, 4.3rem);
64
+ line-height: 1;
65
+ }
66
+
67
+ h2,
68
+ h3,
69
+ p {
70
+ margin-top: 0;
71
+ }
72
+
73
+ .layout {
74
+ display: grid;
75
+ grid-template-columns: 1.15fr 0.85fr;
76
+ gap: 16px;
77
+ }
78
+
79
+ .panel {
80
+ border: 1px solid #d6dde8;
81
+ border-radius: 8px;
82
+ padding: 18px;
83
+ background: white;
84
+ box-shadow: 0 12px 28px rgba(24, 34, 48, 0.06);
85
+ }
86
+
87
+ .input-panel {
88
+ grid-row: span 2;
89
+ }
90
+
91
+ .panel-heading,
92
+ .actions {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: 10px;
97
+ }
98
+
99
+ textarea {
100
+ width: 100%;
101
+ min-height: 430px;
102
+ margin: 12px 0;
103
+ resize: vertical;
104
+ border: 1px solid #cbd5e1;
105
+ border-radius: 6px;
106
+ padding: 14px;
107
+ background: #0f172a;
108
+ color: #dbeafe;
109
+ line-height: 1.5;
110
+ }
111
+
112
+ progress {
113
+ width: 100%;
114
+ margin-top: 14px;
115
+ }
116
+
117
+ .risk-strip {
118
+ display: flex;
119
+ justify-content: space-between;
120
+ padding: 12px;
121
+ border-radius: 6px;
122
+ background: #edf4ff;
123
+ }
124
+
125
+ .risk-strip[data-risk="high"] {
126
+ background: #fff1f3;
127
+ color: #b42318;
128
+ }
129
+
130
+ .risk-strip[data-risk="medium"] {
131
+ background: #fff7e6;
132
+ color: #b54708;
133
+ }
134
+
135
+ .risk-strip[data-risk="low"] {
136
+ background: #ecfdf3;
137
+ color: #067647;
138
+ }
139
+
140
+ .findings {
141
+ display: grid;
142
+ gap: 10px;
143
+ margin-top: 14px;
144
+ }
145
+
146
+ .findings article {
147
+ border: 1px solid #e2e8f0;
148
+ border-radius: 6px;
149
+ padding: 12px;
150
+ }
151
+
152
+ .findings span {
153
+ color: #667085;
154
+ font-size: 0.78rem;
155
+ font-weight: 800;
156
+ text-transform: uppercase;
157
+ }
158
+
159
+ output {
160
+ display: block;
161
+ min-height: 180px;
162
+ white-space: pre-wrap;
163
+ border: 1px solid #e2e8f0;
164
+ border-radius: 6px;
165
+ padding: 12px;
166
+ background: #f8fafc;
167
+ line-height: 1.55;
168
+ }
169
+
170
+ .muted {
171
+ color: #667085;
172
+ }
173
+
174
+ [role="alert"] {
175
+ color: #b42318;
176
+ font-weight: 700;
177
+ }
178
+
179
+ @media (max-width: 900px) {
180
+ header,
181
+ .panel-heading,
182
+ .actions {
183
+ align-items: stretch;
184
+ flex-direction: column;
185
+ }
186
+
187
+ .layout {
188
+ grid-template-columns: 1fr;
189
+ }
190
+ }
@@ -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,24 @@
1
+ # Local Translator (Vite + React)
2
+
3
+ Self-contained example showing:
4
+
5
+ - `useChromeAITranslator`
6
+ - `useChromeAILanguageDetector`
7
+ - support/availability/status/progress/error diagnostics for both APIs
8
+ - source/target language controls
9
+ - textarea input
10
+ - Detect + Translate actions
11
+ - output panel
12
+
13
+ ## Run
14
+
15
+ ```bash
16
+ cd examples/local-translator
17
+ npm install
18
+ npm run dev
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ This example targets Chrome Built-in AI APIs (if available in your browser).
24
+ If translator/detector are unavailable, the cards show availability and status states directly from hooks.
@@ -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 Translator</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,23 @@
1
+ {
2
+ "name": "local-translator-vite-example",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@vitejs/plugin-react": "^4.3.4",
13
+ "@yudin-s/react-chrome-ai": "file:../..",
14
+ "vite": "^6.0.7",
15
+ "typescript": "^5.7.2",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^19.0.0",
21
+ "@types/react-dom": "^19.0.2"
22
+ }
23
+ }
@@ -0,0 +1,235 @@
1
+ import { FormEvent, useMemo, useState } from "react";
2
+ import {
3
+ useChromeAITranslator,
4
+ useChromeAILanguageDetector,
5
+ } from "@yudin-s/react-chrome-ai";
6
+
7
+ type AsyncState = "idle" | "checking" | "unsupported" | "unavailable" | "downloadable" | "downloading" | "preparing" | "ready" | "prompting" | "streaming" | "aborted" | "error";
8
+
9
+ function normalizeLanguage(value: unknown): string | null {
10
+ if (typeof value === "string" && value.trim()) {
11
+ return value.trim();
12
+ }
13
+
14
+ if (Array.isArray(value)) {
15
+ for (const item of value) {
16
+ const candidate = normalizeLanguage(item);
17
+ if (candidate) {
18
+ return candidate;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ if (value && typeof value === "object") {
25
+ const record = value as Record<string, unknown>;
26
+ for (const key of ["language", "detectedLanguage", "lang", "locale"]) {
27
+ const candidate = normalizeLanguage(record[key]);
28
+ if (candidate) {
29
+ return candidate;
30
+ }
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ function statusToPercent(state: number | undefined, progress?: { progress?: number }) {
38
+ if (state != null) {
39
+ return Math.min(100, Math.max(0, Math.round(state * 100)));
40
+ }
41
+
42
+ if (progress?.progress == null) {
43
+ return null;
44
+ }
45
+
46
+ return Math.min(100, Math.max(0, Math.round(progress.progress * 100)));
47
+ }
48
+
49
+ function ApiDiagnostic({
50
+ title,
51
+ supported,
52
+ availability,
53
+ status,
54
+ progress,
55
+ error,
56
+ }: {
57
+ title: string;
58
+ supported: boolean;
59
+ availability?: string;
60
+ status: AsyncState;
61
+ progress?: { progress?: number; indeterminate?: boolean; completed?: boolean };
62
+ error?: Error;
63
+ }) {
64
+ const percent = statusToPercent(progress?.progress, progress);
65
+ const progressText = percent == null ? "in progress" : `${percent}%`;
66
+
67
+ return (
68
+ <section className="api-card">
69
+ <h2>{title}</h2>
70
+ <div className="kv-grid">
71
+ <div>
72
+ <strong>Support</strong>
73
+ <p>{supported ? "Yes" : "No"}</p>
74
+ </div>
75
+ <div>
76
+ <strong>Availability</strong>
77
+ <p>{availability ?? "unknown"}</p>
78
+ </div>
79
+ <div>
80
+ <strong>Status</strong>
81
+ <p>{status}</p>
82
+ </div>
83
+ </div>
84
+ {(status === "downloading" || status === "preparing") && (
85
+ <div className="progress-row">
86
+ <progress max={100} value={percent ?? undefined} />
87
+ <span>{progressText}</span>
88
+ </div>
89
+ )}
90
+ {error && <p className="error">Error: {error.message}</p>}
91
+ </section>
92
+ );
93
+ }
94
+
95
+ export function App() {
96
+ const [sourceText, setSourceText] = useState("Hello. This is a local translator demo.");
97
+ const [sourceLanguage, setSourceLanguage] = useState("en");
98
+ const [targetLanguage, setTargetLanguage] = useState("es");
99
+ const [detectedLanguage, setDetectedLanguage] = useState("");
100
+
101
+ const translator = useChromeAITranslator({
102
+ createOptions: {
103
+ sourceLanguage: sourceLanguage || "en",
104
+ targetLanguage: targetLanguage || "en",
105
+ },
106
+ });
107
+
108
+ const detector = useChromeAILanguageDetector();
109
+ const isBusy = translator.status === "prompting" || translator.status === "streaming";
110
+ const isDetecting = detector.status === "prompting" || detector.status === "streaming";
111
+ const translatedValue = useMemo(() => translator.text.trim(), [translator.text]);
112
+
113
+ const onSubmit = (event: FormEvent) => {
114
+ event.preventDefault();
115
+ void onTranslate();
116
+ };
117
+
118
+ const onDetect = async () => {
119
+ if (!sourceText.trim()) {
120
+ return;
121
+ }
122
+ const detected = await detector.run(sourceText);
123
+ const next = normalizeLanguage(detected);
124
+ if (next) {
125
+ setDetectedLanguage(next);
126
+ setSourceLanguage(next);
127
+ return;
128
+ }
129
+ setDetectedLanguage("Not detected");
130
+ };
131
+
132
+ const onTranslate = async () => {
133
+ if (!sourceText.trim()) {
134
+ return;
135
+ }
136
+ await translator.run(sourceText);
137
+ };
138
+
139
+ return (
140
+ <main className="page">
141
+ <header className="hero">
142
+ <h1>Local Translator Demo</h1>
143
+ <p>Chrome AI Translator + LanguageDetector, working in-browser.</p>
144
+ </header>
145
+
146
+ <div className="grid">
147
+ <ApiDiagnostic
148
+ title="Translator API"
149
+ supported={translator.supported}
150
+ availability={translator.availability}
151
+ status={translator.status}
152
+ progress={translator.progress}
153
+ error={translator.error}
154
+ />
155
+ <ApiDiagnostic
156
+ title="LanguageDetector API"
157
+ supported={detector.supported}
158
+ availability={detector.availability}
159
+ status={detector.status}
160
+ progress={detector.progress}
161
+ error={detector.error}
162
+ />
163
+ </div>
164
+
165
+ <section className="card">
166
+ <form className="form" onSubmit={onSubmit}>
167
+ <label className="inline-field">
168
+ <span>Source language</span>
169
+ <input
170
+ value={sourceLanguage}
171
+ onChange={(event) => setSourceLanguage(event.target.value)}
172
+ placeholder="en"
173
+ aria-label="Source language"
174
+ />
175
+ </label>
176
+ <label className="inline-field">
177
+ <span>Target language</span>
178
+ <input
179
+ value={targetLanguage}
180
+ onChange={(event) => setTargetLanguage(event.target.value)}
181
+ placeholder="es"
182
+ aria-label="Target language"
183
+ />
184
+ </label>
185
+ <label className="input-block">
186
+ <span>Source text</span>
187
+ <textarea
188
+ value={sourceText}
189
+ onChange={(event) => setSourceText(event.target.value)}
190
+ placeholder="Введите текст для перевода"
191
+ aria-label="Source text"
192
+ />
193
+ </label>
194
+ <div className="actions">
195
+ <button type="button" onClick={onDetect} disabled={isDetecting || isBusy}>
196
+ Detect language
197
+ </button>
198
+ <button type="submit" disabled={isBusy || isDetecting}>
199
+ Translate
200
+ </button>
201
+ </div>
202
+ </form>
203
+
204
+ <div className="kv">
205
+ <span>Detected language:</span>
206
+ <strong>{detectedLanguage || "—"}</strong>
207
+ </div>
208
+ <div className="kv">
209
+ <span>Output language:</span>
210
+ <strong>{targetLanguage || "—"}</strong>
211
+ </div>
212
+ </section>
213
+
214
+ <section className="card">
215
+ <h2>Output</h2>
216
+ <output className="output">
217
+ {translator.status === "prompting" || translator.status === "streaming"
218
+ ? "Translating..."
219
+ : translatedValue || "No output yet"}
220
+ </output>
221
+ </section>
222
+
223
+ {translator.error && (
224
+ <p role="alert" className="error">
225
+ Translate error: {translator.error.message}
226
+ </p>
227
+ )}
228
+ {detector.error && (
229
+ <p role="alert" className="error">
230
+ Detect error: {detector.error.message}
231
+ </p>
232
+ )}
233
+ </main>
234
+ );
235
+ }
@@ -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,161 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
4
+ }
5
+
6
+ * {
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ background: #f6f7fb;
14
+ color: #0f172a;
15
+ }
16
+
17
+ #root {
18
+ max-width: 960px;
19
+ margin: 0 auto;
20
+ padding: 2rem 1rem 3rem;
21
+ }
22
+
23
+ .page {
24
+ display: grid;
25
+ gap: 1rem;
26
+ }
27
+
28
+ .hero h1 {
29
+ margin: 0;
30
+ font-size: 1.8rem;
31
+ }
32
+
33
+ .hero p {
34
+ margin: 0.5rem 0 0;
35
+ color: #334155;
36
+ }
37
+
38
+ .grid {
39
+ display: grid;
40
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
41
+ gap: 1rem;
42
+ }
43
+
44
+ .card,
45
+ .api-card {
46
+ background: #ffffff;
47
+ border: 1px solid #d4d8e3;
48
+ border-radius: 8px;
49
+ padding: 1rem;
50
+ }
51
+
52
+ h2 {
53
+ margin: 0 0 0.75rem;
54
+ font-size: 1.1rem;
55
+ }
56
+
57
+ .kv-grid {
58
+ display: grid;
59
+ gap: 0.75rem;
60
+ }
61
+
62
+ .kv-grid > div {
63
+ display: grid;
64
+ gap: 0.25rem;
65
+ }
66
+
67
+ .kv-grid p {
68
+ margin: 0;
69
+ color: #334155;
70
+ }
71
+
72
+ .progress-row {
73
+ margin-top: 0.75rem;
74
+ display: grid;
75
+ gap: 0.5rem;
76
+ }
77
+
78
+ progress {
79
+ width: 100%;
80
+ height: 0.6rem;
81
+ }
82
+
83
+ .form {
84
+ display: grid;
85
+ gap: 0.75rem;
86
+ }
87
+
88
+ .inline-field,
89
+ .input-block {
90
+ display: grid;
91
+ gap: 0.35rem;
92
+ }
93
+
94
+ input,
95
+ textarea {
96
+ border: 1px solid #cfd8e3;
97
+ border-radius: 6px;
98
+ padding: 0.6rem 0.65rem;
99
+ font: inherit;
100
+ color: inherit;
101
+ background: #fff;
102
+ }
103
+
104
+ textarea {
105
+ min-height: 150px;
106
+ resize: vertical;
107
+ }
108
+
109
+ .actions {
110
+ display: flex;
111
+ gap: 0.6rem;
112
+ flex-wrap: wrap;
113
+ }
114
+
115
+ button {
116
+ border: 1px solid #1e293b;
117
+ color: #f8fafc;
118
+ background: #1e293b;
119
+ border-radius: 6px;
120
+ padding: 0.58rem 0.95rem;
121
+ font: inherit;
122
+ font-weight: 600;
123
+ cursor: pointer;
124
+ }
125
+
126
+ button:hover {
127
+ background: #0f172a;
128
+ }
129
+
130
+ button:disabled {
131
+ cursor: not-allowed;
132
+ opacity: 0.6;
133
+ }
134
+
135
+ .kv {
136
+ display: grid;
137
+ grid-template-columns: 1fr 1fr;
138
+ gap: 0.3rem;
139
+ margin-top: 0.85rem;
140
+ padding-top: 0.85rem;
141
+ border-top: 1px solid #e2e8f0;
142
+ }
143
+
144
+ .kv span {
145
+ color: #475569;
146
+ }
147
+
148
+ .output {
149
+ display: block;
150
+ min-height: 130px;
151
+ white-space: pre-wrap;
152
+ background: #f8fafc;
153
+ border: 1px solid #d4d8e3;
154
+ border-radius: 6px;
155
+ padding: 0.8rem;
156
+ }
157
+
158
+ .error {
159
+ color: #b91c1c;
160
+ margin: 0.35rem 0 0;
161
+ }
@@ -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
+ });