Skip to content

Commit 2d65d89

Browse files
committed
wip
1 parent 51c04d8 commit 2d65d89

20 files changed

+284
-93
lines changed
8.91 KB
Loading
763 Bytes
Loading
1.74 KB
Loading
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"manifest_version": 2,
3-
"name": "Thiggle Ext",
3+
"name": "LlamaTab",
44
"version": "1.0",
5-
"description": "This is my awesome Chrome extension.",
5+
"description": "Run a large language model in your browser.",
66
"background": {
77
"scripts": [
88
"models/sentencepiece.js",
@@ -13,8 +13,13 @@
1313
},
1414
"permissions": ["tabs", "storage"],
1515
"browser_action": {
16-
"default_title": "My Extension",
17-
"default_popup": "index.html"
16+
"default_title": "LlamaTab",
17+
"default_popup": "index.html",
18+
"default_icon": {
19+
"16": "icons/icon16.png",
20+
"48": "icons/icon48.png",
21+
"128": "icons/icon128.png"
22+
}
1823
},
1924
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
2025
}

packages/extension/src/App.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,42 @@ import Page from "./Page";
44

55
function App() {
66
const [api, setApi] = useState(null);
7+
const [loadingStatus, setLoadingStatus] = useState({ progress: 0 });
78
useEffect(() => {
89
// eslint-disable-next-line no-undef
910
chrome.runtime.getBackgroundPage((backgroundPage) => {
1011
setApi(backgroundPage.API);
12+
backgroundPage.API.addInitListener(setLoadingStatus);
1113
});
14+
return () => {
15+
// eslint-disable-next-line no-undef
16+
chrome.runtime.getBackgroundPage((backgroundPage) => {
17+
backgroundPage.API.removeInitListener(setLoadingStatus);
18+
});
19+
};
1220
}, []);
21+
1322
if (!api) return null;
1423
return (
1524
<div className="w-[300px]">
1625
<ModelProvider
1726
config={{
1827
api,
28+
persistToLocalStorage: false,
1929
}}
2030
>
2131
<div>
22-
<Page api={api} />
32+
<Page
33+
loadingStatus={loadingStatus}
34+
loadedSystemPrompt={api.systemPrompt}
35+
loadedPromptList={api.promptTemplates}
36+
setPersistedPromptList={(templates) => {
37+
api.promptTemplates = templates;
38+
}}
39+
setPersistedSystemPrompt={(systemPrompt) => {
40+
api.systemPrompt = systemPrompt;
41+
}}
42+
/>
2343
</div>
2444
</ModelProvider>
2545
</div>

packages/extension/src/Page.jsx

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,62 @@
11
"use client";
22
import { useLLM } from "@react-llm/headless";
3-
import { useEffect, useState } from "react";
3+
import { useCallback, useEffect, useState } from "react";
44
import AboutPage from "./pages/AboutPage";
55
import LoadingPage from "./pages/LoadingPage";
66
import MainPage from "./pages/MainPage";
77
import NewPromptPage from "./pages/NewPromptPage";
88
import OptionsPage from "./pages/OptionsPage";
99

10-
export default function Page({ api }) {
11-
const { init, send, setOnMessage, systemPrompt, setSystemPrompt } = useLLM();
10+
export default function Page({
11+
loadingStatus,
12+
loadedSystemPrompt,
13+
loadedPromptList,
14+
setPersistedPromptList,
15+
setPersistedSystemPrompt,
16+
}) {
17+
const {
18+
send,
19+
setOnMessage,
20+
isGenerating,
21+
setConversationPrompt,
22+
conversation,
23+
} = useLLM();
1224
const [text, setText] = useState("");
13-
const [prompt, setPrompt] = useState("");
14-
const [promptList, setPromptList] = useState([]);
1525
const [response, setResponse] = useState("");
1626
const [page, setPage] = useState("main");
1727
const [maxTokens, setMaxTokens] = useState(100);
18-
const [loadingStatus, setLoadingStatus] = useState({});
28+
const [systemPrompt, setSystemPrompt] = useState(loadedSystemPrompt);
29+
const [promptList, setPromptList] = useState(loadedPromptList);
30+
const [prompt, setPrompt] = useState(loadedPromptList && loadedPromptList[0]);
1931

2032
useEffect(() => {
21-
api.addInitListener(setLoadingStatus);
22-
setOnMessage((data) => {
23-
setResponse(data);
24-
});
2533
return () => {
26-
api.removeInitListener(setLoadingStatus);
34+
setPersistedPromptList(promptList);
35+
setPersistedSystemPrompt(systemPrompt);
2736
};
28-
}, [api, init, setOnMessage]);
37+
});
2938

30-
const handleSubmit = (e) => {
31-
e.preventDefault();
32-
send(text, maxTokens);
33-
};
39+
useEffect(() => {
40+
setOnMessage(() => (data) => {
41+
console.log(data);
42+
setResponse(data);
43+
});
44+
}, [setOnMessage]);
45+
46+
useEffect(() => {
47+
if (conversation?.systemPrompt !== systemPrompt) {
48+
setConversationPrompt(systemPrompt);
49+
}
50+
}, [systemPrompt, conversation]);
3451

35-
console.log("progress", loadingStatus);
52+
const handleSubmit = useCallback(
53+
(e) => {
54+
e?.preventDefault();
55+
const tmpl = prompt ? prompt.replace(/\$TEXT/g, text) : text;
56+
send(tmpl, maxTokens);
57+
},
58+
[send, prompt, text, maxTokens]
59+
);
3660

3761
if (loadingStatus.progress < 1) {
3862
return <LoadingPage progress={loadingStatus.progress} />;
@@ -51,8 +75,6 @@ export default function Page({ api }) {
5175
setSystemPrompt={setSystemPrompt}
5276
/>
5377
);
54-
case "load":
55-
return <LoadingPage />;
5678
case "newPrompt":
5779
return (
5880
<NewPromptPage
@@ -72,6 +94,7 @@ export default function Page({ api }) {
7294
response={response}
7395
text={text}
7496
setText={setText}
97+
isGenerating={isGenerating}
7598
/>
7699
);
77100
}

packages/extension/src/background/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ const defaultWorkerConfig = {
1515
maxWindowSize: 2048,
1616
};
1717

18+
const defaultSystemPrompt =
19+
"A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions and always follows the users instructions.";
20+
1821
const API = {
1922
instance: null,
2023
initialized: false,
2124
loadingStatus: {
2225
progress: 0,
2326
},
2427
initListeners: [],
28+
systemPrompt: defaultSystemPrompt,
29+
promptTemplates: ["$TEXT"],
2530
addInitListener(cb) {
2631
this.initListeners.push(cb);
2732
cb(this.loadingStatus);
@@ -45,6 +50,7 @@ const API = {
4550
});
4651
},
4752
generate(request, cb = console.log) {
53+
console.log("generate request: ", request);
4854
this.instance?.generate(request, (resp) => {
4955
console.log("generate: ", resp);
5056
cb(resp);

packages/extension/src/pages/LoadingPage.jsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import { useLLM } from "@react-llm/headless";
22

33
const LoadingPage = ({ progress }) => {
4-
const { init } = useLLM();
4+
const { init, gpuDevice } = useLLM();
55

6+
if (gpuDevice?.unsupportedReason !== null && gpuDevice?.checked) {
7+
return (
8+
<div className="flex flex-col gap-2 p-4 text-sm">
9+
<p>
10+
{JSON.stringify(gpuDevice)}
11+
Sorry! LLamaTab is not supported on your device. Reason $
12+
{gpuDevice.unsupportedReason}. LLamaTab requires a device with WebGPU
13+
support.
14+
</p>
15+
</div>
16+
);
17+
}
618
if (progress === 0)
719
return (
820
<div className="flex flex-col gap-2 p-4 text-sm">

packages/extension/src/pages/MainPage.jsx

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React, { useEffect } from "react";
2+
import TextArea from "./Textarea";
13
const MainPage = ({
24
prompt,
35
setPrompt,
@@ -7,7 +9,24 @@ const MainPage = ({
79
response,
810
text,
911
setText,
12+
isGenerating,
1013
}) => {
14+
useEffect(() => {
15+
const handleKeyPress = (event) => {
16+
console.log(event);
17+
if (event.metaKey && event.key === "Enter") {
18+
event.preventDefault();
19+
handleSubmit();
20+
}
21+
};
22+
23+
document.addEventListener("keydown", handleKeyPress);
24+
25+
return () => {
26+
document.removeEventListener("keydown", handleKeyPress);
27+
};
28+
}, [handleSubmit]);
29+
1130
return (
1231
<form onSubmit={handleSubmit}>
1332
<div className="flex flex-col m-auto p-3 text-sm gap-2 relative">
@@ -19,60 +38,64 @@ const MainPage = ({
1938
</div>
2039
<div>
2140
<div>Text</div>
22-
<textarea
23-
className="w-full rounded-md border border-blue-500 p-2 resize-none"
24-
value={text}
25-
onChange={(e) => setText(e.target.value)}
26-
/>
41+
<TextArea value={text} onChange={(e) => setText(e.target.value)} />
2742
</div>
2843
<div>
2944
<div>Prompt Template</div>
3045
<select
31-
className="w-full rounded-md border border-blue-500 p-1"
32-
value={prompt}
46+
className="w-full rounded-md border border-gray-300 p-1"
47+
defaultValue={prompt}
3348
onChange={(e) => setPrompt(e.target.value)}
3449
>
35-
{promptList.map((prompt, idx) => (
36-
<option key={idx} value={prompt}>
37-
{prompt}
50+
{promptList.map((item, idx) => (
51+
<option key={idx} value={item}>
52+
{item}
3853
</option>
3954
))}
4055
</select>
4156
</div>
42-
<div className="flex w-full items-center">
43-
<div className="flex-grow flex gap-2 flex-col">
57+
<div className="flex w-full items-center">
58+
<div className="flex-grow flex gap-2 justify-center m-2">
4459
<div>
4560
<button
46-
className="text-blue-500 "
61+
className="text-blue-500"
4762
onClick={() => setPage("newPrompt")}
4863
>
49-
Add New Prompt
64+
Manage templates
5065
</button>
5166
</div>
52-
<div className="flex">
67+
<div className="">
5368
<button
5469
className="self-end text-blue-500"
5570
onClick={() => setPage("options")}
5671
>
5772
Advanced Options
5873
</button>
5974
</div>
75+
<div className="self-end flex flex-col justify-center">
76+
<button
77+
type="submit"
78+
className="self-end rounded bg-blue-500 px-2 py-2 font-bold text-white hover:bg-blue-700"
79+
>
80+
Generate
81+
</button>
82+
<div className="text-xs text-gray-400 m-1">(⌘+Enter)</div>
83+
</div>
6084
</div>
85+
</div>
6186

62-
<div>
63-
<button
64-
type="submit"
65-
className="self-end rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
66-
>
67-
Generate
68-
</button>
87+
{isGenerating && (
88+
<div className="flex flex-col gap-2 items-center py-3">
89+
<div className="self-start">
90+
<p className="text-bold">Thinking... </p>
91+
</div>
6992
</div>
70-
</div>
93+
)}
7194

7295
{response && (
7396
<div>
74-
<div>Response</div>
75-
<div className="border h-[50px] rounded-md"></div>
97+
<div>Assistant</div>
98+
<div className="border rounded-md p-3">{response.outputText}</div>
7699
</div>
77100
)}
78101
</div>

packages/extension/src/pages/NewPromptPage.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from "react";
2+
import TextArea from "./Textarea";
23
const NewPromptPage = ({ setPage, promptList, setPromptList }) => {
34
const [newPrompt, setNewPrompt] = useState("");
45

@@ -17,7 +18,7 @@ const NewPromptPage = ({ setPage, promptList, setPromptList }) => {
1718

1819
return (
1920
<div className="flex flex-col gap-2 p-4 text-sm">
20-
<div className="self-end mb-4">
21+
<div className="self-end">
2122
<button
2223
className="text-blue-500 hover:underline"
2324
onClick={() => setPage("main")}
@@ -27,12 +28,12 @@ const NewPromptPage = ({ setPage, promptList, setPromptList }) => {
2728
</div>
2829

2930
<div className="mb-4">
30-
<input
31-
className="w-full rounded-md border border-blue-500 p-2"
32-
type="text"
31+
<p className="mb-2">
32+
A template for the prompt. $TEXT will be replaced by the input text.
33+
</p>
34+
<TextArea
3335
value={newPrompt}
3436
onChange={(e) => setNewPrompt(e.target.value)}
35-
placeholder="Add new prompt"
3637
/>
3738
<div className="flex justify-end">
3839
<button

0 commit comments

Comments
 (0)