├── .dockerignore
├── .eslintrc.json
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── bin
└── config.js
├── components
├── ChatHistory.jsx
├── Chatbox.jsx
├── Chats.jsx
├── Dialog.jsx
└── ErrorDisplay.jsx
├── doc
├── screenshot_0.png
├── screenshot_1.png
├── screenshot_2.png
├── win_0.png
├── win_1.png
├── win_2.png
├── win_3.png
├── win_4.png
├── win_5.png
├── win_6.png
├── win_7.png
├── win_8.png
└── windows.md
├── docker-compose.yml
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── _document.js
├── api
│ ├── chats.js
│ ├── messages.js
│ └── socketio.js
└── index.js
├── postcss.config.js
├── public
└── favicon.ico
├── styles
├── Home.module.css
└── globals.css
├── tailwind.config.js
└── utils
├── AppContext.js
├── conversation.js
├── db.js
├── lang-detector.js
├── native.js
├── nl2br.js
└── uuid.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .data
3 | .next
4 | node_modules
5 | bin
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .env
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | /bin/*
36 | !/bin/config.js
37 | !/bin/run-chat.sh
38 | .data
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.15.0-alpine3.17
2 |
3 | RUN apk add gcompat
4 |
5 | ENV NODE_ENV=production
6 | WORKDIR /app
7 |
8 | COPY package.json .
9 | COPY package-lock.json .
10 |
11 | RUN npm i
12 |
13 | COPY . .
14 |
15 | # volumes need to be mounted bind mount
16 | VOLUME [ "/app/.data" ]
17 | VOLUME [ "/app/bin" ]
18 |
19 | CMD npm start
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Xuan Son Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | -----
24 |
25 | MIT License
26 |
27 | Copyright (c) 2023 Aidan Guarniere
28 |
29 | Permission is hereby granted, free of charge, to any person obtaining a copy
30 | of this software and associated documentation files (the "Software"), to deal
31 | in the Software without restriction, including without limitation the rights
32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
33 | copies of the Software, and to permit persons to whom the Software is
34 | furnished to do so, subject to the following conditions:
35 |
36 | The above copyright notice and this permission notice shall be included in all
37 | copies or substantial portions of the Software.
38 |
39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
45 | SOFTWARE.
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alpaca.cpp Web UI (Next.js)
2 |
3 | This is a web UI wrapper for alpaca.cpp
4 |
5 | Thanks to:
6 | - [github.com/AidanGuarniere/chatGPT-UI-template](https://github.com/AidanGuarniere/chatGPT-UI-template)
7 | - [github.com/antimatter15/alpaca.cpp](https://github.com/antimatter15/alpaca.cpp) and [github.com/ggerganov/llama.cpp](https://github.com/ggerganov/llama.cpp)
8 | - [github.com/nomic-ai/gpt4all](https://github.com/nomic-ai/gpt4all)
9 | - [Suggestion for parameters](https://github.com/antimatter15/alpaca.cpp/issues/171)
10 |
11 | ## Features
12 |
13 | - [x] Save chat history to disk
14 | - [x] Implement context memory
15 | - [x] Conversation history
16 | - [x] Interface for tweaking parameters
17 | - [x] Better guide / documentation
18 | - [x] Ability to stop / regenerate response
19 | - [x] Detect code response / use monospace font
20 | - [x] Responsive UI
21 | - [ ] [Configuration presets](https://www.reddit.com/r/LocalLLaMA/comments/1227uj5/my_experience_with_alpacacpp/)
22 |
23 | Screenshot:
24 |
25 | 
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## How to use
33 |
34 | Pre-requirements:
35 | - You have nodejs v18+ installed on your machine (or if you have Docker, you don't need to install nodejs)
36 | - You are using Linux (Windows should also work, but I have not tested yet)
37 |
38 | **For Windows user**, these is a detailed guide here: [doc/windows.md](./doc/windows.md)
39 |
40 | 🔶 **Step 1**: Clone this repository to your local machine
41 |
42 | 🔶 **Step 2**: Download the model and binary file to run the model. You have some options:
43 |
44 | - 👉 (Recommended) `Alpaca.cpp` and `Alpaca-native-4bit-ggml` model => This combination give me very convincing responses most of the time
45 | - Download `chat` binary file and place it under `bin` folder: https://github.com/antimatter15/alpaca.cpp/releases
46 | - Download `ggml-alpaca-7b-q4.bin` and place it under `bin` folder: https://huggingface.co/Sosaka/Alpaca-native-4bit-ggml/blob/main/ggml-alpaca-7b-q4.bin
47 |
48 | - 👉 Alternatively, you can use `gpt4all`: Download `gpt4all-lora-quantized.bin` and `gpt4all-lora-quantized-*-x86` from [github.com/nomic-ai/gpt4all](https://github.com/nomic-ai/gpt4all), put them into `bin` folder
49 |
50 | 🔶 **Step 3**: Edit `bin/config.js` so that the executable name and the model file name are correct
51 | (If you are using `chat` and `ggml-alpaca-7b-q4.bin`, you don't need to modify anything)
52 |
53 | 🔶 **Step 4**: Run these commands
54 |
55 | ```
56 | npm i
57 | npm start
58 | ```
59 |
60 | Alternatively, you can just use `docker compose up` if you have Docker installed.
61 |
62 | Then, open [http://localhost:13000/](http://localhost:13000/) on your browser
63 |
64 | ## TODO
65 |
66 | - [x] Test on Windows
67 | - [x] Proxy ws via nextjs
68 | - [x] Add Dockerfile / docker-compose
69 | - [ ] UI: add avatar
--------------------------------------------------------------------------------
/bin/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | // The ./chat can be downloaded from alpaca.cpp
3 | EXECUTABLE_FILE: './chat',
4 | MODEL_FILE: './ggml-alpaca-7b-q4.bin',
5 |
6 | // Alternatively, the gpt4all-lora-quantized can be downloaded from nomic-ai/gpt4all
7 | // BIN_FILE: './gpt4all-lora-quantized-linux-x86',
8 | // MODEL_FILE :'./gpt4all-lora-quantized.bin',
9 | }
10 |
11 | module.exports = config;
--------------------------------------------------------------------------------
/components/ChatHistory.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import axios from "axios";
3 | import { useAppContext } from "../utils/AppContext";
4 | import Dialog from "./Dialog";
5 |
6 | const IconEdit = ({ className, onClick }) =>
7 |
8 | const IconDelete = ({ className, onClick }) =>
9 |
10 | const IconUser = ({ className, onClick }) =>
11 |
12 | const IconOpen = ({ className, onClick }) =>
13 |
14 | function ChatHistory() {
15 | const {
16 | chats,
17 | userText,
18 | setUserText,
19 | setChats,
20 | selectedChat,
21 | setSelectedChat,
22 | showMenu,
23 | setShowMenu,
24 | } = useAppContext();
25 |
26 | const [showSettings, setShowSettings] = useState(false);
27 |
28 | const fetchChats = async () => {
29 | try {
30 | const response = await axios.get("/api/chats");
31 | if (response.data) {
32 | setChats(response.data);
33 | }
34 | } catch (error) {
35 | console.error("Error fetching chats:", error);
36 | }
37 | };
38 |
39 | const deleteChats = async (id) => {
40 | if (id) {
41 | await axios.delete("/api/chats", { data: { id } });
42 | fetchChats();
43 | } else {
44 | await axios.delete("/api/chats");
45 | setChats([]);
46 | }
47 | setSelectedChat(null);
48 | };
49 |
50 | const createChat = async () => {
51 | const { data } = await axios.post("/api/chats");
52 | fetchChats();
53 | setSelectedChat(data.chat_id);
54 | };
55 |
56 | const editChat = async (id, title) => {
57 | await axios.patch("/api/chats", { id, title });
58 | fetchChats();
59 | };
60 |
61 | const handleClickDelete = async (chat) => {
62 | if (window.confirm(`Are you sure to delete "${chat.title}"?`)) {
63 | deleteChats(chat.id);
64 | }
65 | };
66 |
67 | const handleClickEdit = async (chat) => {
68 | const newTitle = window.prompt('Change title', chat.title);
69 | if (newTitle) {
70 | await editChat(chat.id, newTitle);
71 | fetchChats();
72 | }
73 | };
74 |
75 | return <>
76 |
77 |
78 |
79 |
80 | {
83 | if (userText.length) {
84 | setUserText("");
85 | }
86 | createChat();
87 | setShowMenu(false);
88 | }}
89 | >
90 |
102 |
103 |
104 |
105 | New chat
106 |
107 |
108 |
109 | {chats.map((chat, index) => (
110 |
{
118 | if (userText.length) {
119 | setUserText("");
120 | }
121 | setSelectedChat(chat.id);
122 | setShowMenu(false);
123 | }}
124 | >
125 |
137 |
138 |
139 |
140 | {chat.title}
141 |
142 |
143 | {chat.id === selectedChat &&
144 |
145 | handleClickEdit(chat)} />
146 | handleClickDelete(chat)} />
147 |
148 | }
149 |
150 | ))}
151 |
152 |
153 | {chats.length > 0 && <>
154 | {
157 | if (window.confirm('This will delete all conversations. Are you sure?')) {
158 | deleteChats();
159 | }
160 | }}
161 | >
162 |
163 | Clear conversations
164 |
165 | >}
166 | {
169 | setShowSettings(true);
170 | }}
171 | >
172 |
173 | Settings
174 |
175 |
176 |
177 |
178 |
179 | {showSettings &&
}
180 |
181 |
182 | {/* Mobile top bar */}
183 | {!showMenu &&
186 | setShowMenu(true)} />
187 | {selectedChat && chats.find(c => c.id === selectedChat)?.title}
188 |
}
189 | >;
190 | }
191 |
192 | const MenuBurgerIcon = ({ className, onClick }) =>
193 |
194 | export default ChatHistory;
195 |
--------------------------------------------------------------------------------
/components/Chatbox.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { uuidv4 } from "../utils/uuid";
4 | import { useAppContext } from "../utils/AppContext";
5 | import ErrorDisplay from "./ErrorDisplay";
6 | import { getConversationPrompt } from "../utils/conversation";
7 |
8 | function Chatbox({ chatRef }) {
9 | const {
10 | error,
11 | setError,
12 | userText,
13 | setUserText,
14 | chats,
15 | setChats,
16 | selectedChat,
17 | setSelectedChat,
18 | assistantTypingMsgId,
19 | setAssistantTypingMsgId,
20 | socket,
21 | userConfig,
22 | } = useAppContext();
23 |
24 | const loading = !!assistantTypingMsgId;
25 |
26 | const handleChange = (event) => {
27 | setUserText(event.target.value);
28 | setError(null);
29 | };
30 |
31 | const handleSubmit = async (event) => {
32 | event.preventDefault();
33 | if (loading) return;
34 | if (userText.length >= 2) {
35 | const selectedIndex = chats.findIndex(
36 | (chat) => chat.id === selectedChat
37 | );
38 | const selectedChatData = chats[selectedIndex];
39 | //console.log(selectedChatData);
40 |
41 | // add message
42 | const newAssistantMsgId = uuidv4();
43 | const newMsgUser = {
44 | id: uuidv4(),
45 | chat_id: selectedChat,
46 | role: 'user',
47 | content: userText,
48 | createdAt: Date.now(),
49 | }
50 | await axios.post('/api/messages', newMsgUser).catch(console.error);
51 | const newMsgAssitant = {
52 | id: newAssistantMsgId,
53 | chat_id: selectedChat,
54 | role: 'assistant',
55 | content: '',
56 | createdAt: Date.now(),
57 | }
58 | const newChat = {
59 | ...selectedChatData,
60 | messages: [
61 | ...selectedChatData.messages,
62 | newMsgUser,
63 | newMsgAssitant,
64 | ],
65 | };
66 | const userPrompt = userConfig['__context_memory'] === '0'
67 | ? userText
68 | : getConversationPrompt(selectedChatData.messages, userText, userConfig['__context_memory'], userConfig['__context_memory_prompt']);
69 | setChats(chats => chats.map(c => c.id === newChat.id ? newChat : c));
70 | setAssistantTypingMsgId(newAssistantMsgId);
71 |
72 | // send request to backend
73 | const req = {
74 | chatId: selectedChat,
75 | messageId: newAssistantMsgId,
76 | input: userPrompt,
77 | };
78 | socket.emit('ask', req);
79 | setUserText('');
80 |
81 | // save to backend
82 | await axios.post('/api/messages', newMsgAssitant).catch(console.error);
83 | } else {
84 | setError("Please enter a valid prompt");
85 | }
86 | };
87 |
88 | const handleKeyPress = (event) => {
89 | if (event.key === "Enter" && !event.shiftKey) {
90 | event.preventDefault();
91 | handleSubmit(event);
92 | // reset height
93 | const target = event.target;
94 | setTimeout(() => {
95 | target.style.height = "auto";
96 | target.style.height = `${target.scrollHeight}px`;
97 | }, 50);
98 | }
99 | }
100 |
101 | return (
102 | //
103 |
107 | {loading &&
108 |
{
109 | socket.emit('action_stop', {});
110 | }}>
111 |
112 |
113 | Stop generate
114 |
115 |
116 |
}
117 |
118 | {error &&
119 |
120 |
}
121 |
122 |
178 |
179 | );
180 | }
181 |
182 | export default Chatbox;
183 |
184 |
185 | const StopIcon = () =>
186 |
187 | const RefreshIcon = () =>
--------------------------------------------------------------------------------
/components/Chats.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
2 | import { useAppContext } from "../utils/AppContext";
3 | import { nl2br } from "../utils/nl2br";
4 | import Chatbox from "./Chatbox";
5 | import detectLang from "../utils/lang-detector";
6 |
7 | function Chats() {
8 | const {
9 | userText,
10 | setUserText,
11 | setError,
12 | chats,
13 | setChats,
14 | selectedChat,
15 | setSelectedChat,
16 | assistantTypingMsgId,
17 | setAssistantTypingMsgId,
18 | } = useAppContext();
19 |
20 | const chatRef = useRef(null);
21 | const [scrollHeight, setScrollHeight] = useState();
22 | const [prevMsgCount, setPrevMsgCount] = useState(-1);
23 | const scrollToBottom = useCallback(() => {
24 | if (chatRef.current) {
25 | chatRef.current.scrollTo({
26 | top: chatRef.current.scrollHeight,
27 | behavior: "smooth", // use "auto" for instant scrolling
28 | });
29 | }
30 | }, []);
31 |
32 | const msgContainsCode = useMemo(() => {
33 | const chat = chats.find(c => c.id === selectedChat);
34 | if (!chat) return {};
35 | const res = {};
36 | chat.messages.forEach(m => res[m.id] = delectIsCode(m.content));
37 | return res;
38 | }, [chats, selectedChat]);
39 |
40 | useEffect(() => {
41 | scrollToBottom();
42 | }, [selectedChat, scrollToBottom]);
43 |
44 | useEffect(() => {
45 | const chat = chats.find(c => c.id === selectedChat);
46 | if (!chat) return;
47 | const msgCount = chat.messages.length;
48 | if (msgCount !== prevMsgCount) {
49 | scrollToBottom();
50 | setPrevMsgCount(msgCount);
51 | }
52 | }, [chats, selectedChat, prevMsgCount, scrollToBottom]);
53 |
54 | return (
55 |
56 |
57 | {/*
Model: Default (GPT-3.5)
*/}
58 |
59 | {selectedChat !== null &&
60 | chats[chats.findIndex((chat) => chat.id === selectedChat)] ? (
61 |
{
66 | setScrollHeight(chatRef.current.scrollTop);
67 | }}
68 | >
69 |
70 |
71 | {chats[
72 | chats.findIndex((chat) => chat.id === selectedChat)
73 | ].messages.map((message, index) =>
74 | message.role === "system" ? null : (
75 |
82 | {message.role === "assistant" ? (
83 | <>>// ChatGPT:
84 | ) : null}
85 | {msgContainsCode[message.id]
86 | ? {nl2br(message.content)}
87 | : nl2br(message.content)
88 | }
89 | {assistantTypingMsgId === message.id &&
90 | {
91 | !!(message.content || '').length ? <> > : null
92 | }▌
93 | }
94 |
95 | )
96 | )}
97 |
98 | {/* scroll-to-bottom button */}
99 | {chatRef.current
100 | ? scrollHeight + chatRef.current.clientHeight * 1.1 <
101 | chatRef.current.scrollHeight && (
102 |
{
105 | if (chatRef.current) {
106 | chatRef.current.scrollTo({
107 | top: chatRef.current.scrollHeight,
108 | behavior: "smooth", // use "auto" for instant scrolling
109 | });
110 | }
111 | }}
112 | >
113 |
125 |
126 |
127 |
128 |
129 | )
130 | : null}
131 |
132 | ) : (
133 |
134 | Alpaca.cpp
135 |
136 | )}
137 |
138 |
139 |
140 |
141 | );
142 | }
143 |
144 | const CACHE_DETECT_CODE = {}
145 | const delectIsCode = (text) => {
146 | if (text.length < 10) return false;
147 | const _txt = text.substr(0, 80);
148 | if (CACHE_DETECT_CODE[_txt] !== undefined) {
149 | return CACHE_DETECT_CODE[_txt];
150 | } else {
151 | const res = detectLang(_txt);
152 | const isCode = res.language !== 'Unknown' && res.points >= 2;
153 | CACHE_DETECT_CODE[_txt] = isCode;
154 | }
155 | };
156 |
157 | export default Chats;
158 |
--------------------------------------------------------------------------------
/components/Dialog.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAppContext } from "../utils/AppContext";
3 |
4 | const Dialog = {
5 | DownloadModelAndBinary: ({ error }) => {
6 | const { modelPathAbs, pathExecAbs } = error;
7 |
8 | return (
9 |
12 |
13 | Cannot find ggml model file or executable file under {'{YOUR_PROJECT_DIRECTORY}/bin'}
14 |
15 | Please follow this guide to download the model:
16 | https://github.com/ngxson/alpaca.cpp-webui#how-to-use
17 |
18 | {!pathExecAbs.startsWith('/app/bin') && <>
19 | {/* only show this part outside docker container */}
20 | Then, make sure that these paths exist:
21 | {pathExecAbs}
22 | {modelPathAbs}
23 | >}
24 |
25 | >}
26 | />
27 | );
28 | },
29 | ErrorCannotConnectWS: () => {
30 | return (
31 |
34 |
35 | Make sure that the process is still running
36 |
37 | >}
38 | />
39 | );
40 | },
41 | Settings: ({ setShowSettings }) => {
42 | const { socket, userConfig } = useAppContext();
43 | const [cfg, setCfg] = useState(JSON.parse(JSON.stringify(userConfig)));
44 | const dismiss = () => setShowSettings(false);
45 |
46 | const onChange = (key) => (e) => { setCfg(cfg => ({ ...cfg, [key]: e.target.value })) };
47 |
48 | const CONFIGS = [
49 | 'threads',
50 | 'seed',
51 | 'top_k',
52 | 'top_p',
53 | 'n_predict',
54 | 'temp',
55 | 'repeat_penalty',
56 | 'ctx_size',
57 | 'repeat_last_n',
58 | ];
59 | const CLASS_DISABLE = "md:w-1/2 border-2 border-gray-800 text-gray-400 rounded p-3 mr-1 cursor-pointer";
60 | const CLASS_ENABLE = "md:w-1/2 border-2 border-emerald-600 text-gray-200 rounded p-3 mr-1 cursor-pointer";
61 | const handleSetContextMemory = (enabled) => () => {
62 | setCfg(cfg => ({ ...cfg, '__context_memory': enabled ? '4' : '0' }));
63 | };
64 |
65 | return (
66 |
69 |
70 | {CONFIGS.map(key =>
)}
76 |
81 | {/*
*/}
88 | {cfg['__context_memory'] !== '0' && (
89 |
94 | )}
95 | {cfg['__context_memory'] !== '0' && (
96 |
102 | )}
103 |
104 |
105 |
No context memory
106 |
The model doesn't care about previous messages
107 |
108 |
109 |
110 |
Context memory
111 |
The model remember last N messages. Reponse time will be much slower.
112 |
113 |
114 |
115 |
116 | >}
117 | footer={<>
118 |
119 | After saving changes, you will need to manually restart the server.
120 |
121 |
122 |
127 | Cancel
128 |
129 | {
133 | socket.emit('save_user_config', cfg);
134 | window.alert('Changes saved. Please manually restart the server.');
135 | dismiss();
136 | }}
137 | >
138 | Save Changes
139 |
140 | >}
141 | />
142 | );
143 | },
144 | Wrapper: ({ title, content, footer }) => {
145 | return <>
146 |
150 |
151 |
155 |
156 | {/*content*/}
157 |
158 | {/*header*/}
159 |
160 |
161 | {title}
162 |
163 |
164 | {/*body*/}
165 |
166 | {content}
167 |
168 | {/*footer*/}
169 | {footer &&
170 | {footer}
171 |
}
172 |
173 |
174 |
175 |
176 | >;
177 | },
178 | };
179 |
180 | const FormInput = ({ label, value, onChange, type = 'input', placeholder }) => {
181 | return <>
182 |
183 |
184 |
185 | {label}
186 |
187 |
188 |
189 | {type === 'input' && }
190 | {type === 'textarea' && }
196 |
197 |
198 | >
199 | };
200 |
201 | export default Dialog;
--------------------------------------------------------------------------------
/components/ErrorDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 |
3 | function ErrorDisplay({ error }) {
4 | const _err = useMemo(
5 | () => (typeof error === 'string' || error instanceof String)
6 | ? new Error(error)
7 | : error,
8 | [error]
9 | )
10 |
11 | return (
12 |
13 | {" "}
14 | {_err.message && (
15 |
Error: {_err.message}
16 | )}
17 | {_err.code && (
18 |
19 | Error code: {_err.code}
20 |
21 | )}
22 |
23 | );
24 | }
25 |
26 | export default ErrorDisplay;
27 |
--------------------------------------------------------------------------------
/doc/screenshot_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/screenshot_0.png
--------------------------------------------------------------------------------
/doc/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/screenshot_1.png
--------------------------------------------------------------------------------
/doc/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/screenshot_2.png
--------------------------------------------------------------------------------
/doc/win_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_0.png
--------------------------------------------------------------------------------
/doc/win_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_1.png
--------------------------------------------------------------------------------
/doc/win_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_2.png
--------------------------------------------------------------------------------
/doc/win_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_3.png
--------------------------------------------------------------------------------
/doc/win_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_4.png
--------------------------------------------------------------------------------
/doc/win_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_5.png
--------------------------------------------------------------------------------
/doc/win_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_6.png
--------------------------------------------------------------------------------
/doc/win_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_7.png
--------------------------------------------------------------------------------
/doc/win_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/doc/win_8.png
--------------------------------------------------------------------------------
/doc/windows.md:
--------------------------------------------------------------------------------
1 | # Guide for Windows
2 |
3 | Install nodejs v18+:
4 |
5 | 
6 |
7 | Download the windows build for alpaca.cpp:
8 |
9 | 
10 |
11 | Go to `{PROJECT_DIR}/bin`
12 |
13 | 
14 |
15 | Extract the zip file
16 |
17 | 
18 |
19 | You should have these files in `bin` folder
20 |
21 | 
22 |
23 | Edit the `config.js`
24 |
25 | 
26 |
27 | Add `.exe` to the executable name:
28 |
29 | 
30 |
31 | Go back to `{PROJECT_DIR}`
32 |
33 | Open a new terminal and run `npm i`
34 |
35 | 
36 |
37 | Run `npm start`
38 |
39 | If you see error `child process exited with code ...`, make sure that you have enough available RAM to load the model. (For 7B, it's recommended to have at least 8GB of RAM)
40 |
41 | 
42 |
43 | Then, visit `http://localhost:13000`
44 |
45 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | alpaca_webui:
5 | build: .
6 | container_name: alpaca_webui
7 | hostname: alpaca_webui
8 | ports:
9 | - 13000:13000
10 | volumes:
11 | - ./.data:/app/.data
12 | - ./bin:/app/bin
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | env: {
5 | NEXT_PUBLIC_OPENAI_API_KEY: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgpt-ui-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "cross-env PORT=3001 WS_PORT=13001 concurrently --kill-others \"npm run proc_dev\" \"npm run proc_native\"",
7 | "proc_dev": "next dev",
8 | "proc_serve": "next start",
9 | "proc_native": "node ./utils/native.js",
10 | "build": "next build",
11 | "start": "next build && cross-env PORT=13000 WS_PORT=13001 concurrently --kill-others \"npm run proc_serve\" \"npm run proc_native\"",
12 | "lint": "next lint"
13 | },
14 | "dependencies": {
15 | "@next/font": "13.1.1",
16 | "autoprefixer": "^10.4.13",
17 | "axios": "^1.2.2",
18 | "concurrently": "^8.0.1",
19 | "cross-env": "^7.0.3",
20 | "dotenv": "^16.0.3",
21 | "eslint": "8.31.0",
22 | "eslint-config-next": "13.1.1",
23 | "express": "^4.18.2",
24 | "next": "13.1.1",
25 | "postcss": "^8.4.21",
26 | "react": "18.2.0",
27 | "react-dom": "18.2.0",
28 | "sequelize": "^6.30.0",
29 | "socket.io": "^4.6.1",
30 | "socket.io-client": "^4.6.1",
31 | "sqlite3": "^5.1.6",
32 | "string-argv": "^0.3.1",
33 | "tailwindcss": "^3.2.6",
34 | "underscore": "^1.13.6",
35 | "uuid": "^9.0.0",
36 | "uuidv4": "^6.2.13"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 |
3 | export default function App({ Component, pageProps }) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/pages/api/chats.js:
--------------------------------------------------------------------------------
1 | // pages/api/chats.js
2 |
3 | import { ChatModel, MessageModel } from "../../utils/db";
4 | import { uuidv4 } from "../../utils/uuid";
5 |
6 | const getMessageObj = (id, chat_id, data) => {
7 | return {
8 | id,
9 | chat_id,
10 | ...JSON.parse(data),
11 | }
12 | };
13 |
14 | export const getAllChats = async () => {
15 | const [
16 | allChatsMetadata,
17 | allMessages,
18 | ] = await Promise.all([
19 | ChatModel.findAll(),
20 | MessageModel.findAll(),
21 | ]);
22 | const metadatas = Object.fromEntries(
23 | allChatsMetadata.map(({id, title}) => [id, {title}])
24 | );
25 | const chats = {};
26 | allMessages.forEach(({ id, chat_id, data }) => {
27 | if (!chats[chat_id]) {
28 | const metadata = metadatas[chat_id] || {};
29 | chats[chat_id] = {
30 | id: chat_id,
31 | title: metadata.title || 'New Chat',
32 | messages: []
33 | };
34 | }
35 | chats[chat_id].messages.push(getMessageObj(id, chat_id, data));
36 | });
37 |
38 | const allChats = Object.values(chats);
39 | allChats.sort((a,b) => b.messages?.at(-1)?.createdAt - a.messages?.at(-1)?.createdAt);
40 |
41 | return allChats;
42 | };
43 |
44 | export default async function handler(req, res) {
45 | const { method } = req;
46 |
47 | switch (method) {
48 | case "GET":
49 | res.status(200).json(await getAllChats());
50 | break;
51 | case "POST":
52 | try {
53 | const msg = {
54 | id: uuidv4(),
55 | chat_id: uuidv4(),
56 | data: JSON.stringify({
57 | role: 'assistant',
58 | content: 'Hi, how can I help you?',
59 | createdAt: Date.now(),
60 | })
61 | };
62 | await MessageModel.bulkCreate([msg]);
63 | await ChatModel.bulkCreate([{
64 | id: msg.chat_id,
65 | title: 'New Chat',
66 | data: '{}',
67 | }]);
68 | return res.status(201).json(getMessageObj(msg.id, msg.chat_id, msg.data));
69 | } catch (error) {
70 | console.error(error);
71 | res.status(500).json({ error: "Internal Server Error" });
72 | }
73 | break;
74 | case "PATCH":
75 | try {
76 | const { id, title } = req.body;
77 | const row = await ChatModel.findOne({ where: { id } });
78 | row.title = title;
79 | await row.save();
80 | return res.status(201).json({});
81 | } catch (error) {
82 | console.error(error);
83 | res.status(500).json({ error: "Internal Server Error" });
84 | }
85 | break;
86 | case "DELETE":
87 | try {
88 | const { id } = req.body;
89 | if (id) {
90 | await MessageModel.destroy({ where: { chat_id: id } });
91 | await ChatModel.destroy({ where: { id } });
92 | } else {
93 | await MessageModel.destroy({ where: {} });
94 | await ChatModel.destroy({ where: {} });
95 | }
96 | return res.status(201).json({});
97 | } catch (error) {
98 | console.error(error);
99 | res.status(500).json({ error: "Internal Server Error" });
100 | }
101 | break;
102 | default:
103 | res.setHeader("Allow", ["GET", "POST", "PATCH", "DELETE"]);
104 | res.status(405).end(`Method ${method} Not Allowed`);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/pages/api/messages.js:
--------------------------------------------------------------------------------
1 | // pages/api/chats.js
2 |
3 | import { MessageModel } from "../../utils/db";
4 |
5 | const getMessageObj = (id, chat_id, data) => {
6 | return {
7 | id,
8 | chat_id,
9 | ...JSON.parse(data),
10 | }
11 | };
12 |
13 | export default async function handler(req, res) {
14 | const { method } = req;
15 |
16 | switch (method) {
17 | case "GET":
18 | try {
19 | const { id } = req.query;
20 | const row = await MessageModel.findOne({ where: { id } });
21 | if (row) {
22 | res.status(200).json(getMessageObj(row.id, row.chat_id, row.data));
23 | } else {
24 | res.status(404).json({ error: 'not found' });
25 | }
26 | } catch (error) {
27 | console.error(error);
28 | res.status(500).json({ error: "Internal Server Error" });
29 | }
30 | break;
31 | case "POST":
32 | try {
33 | const { id, chat_id, role, content } = req.body;
34 | const row = await MessageModel.findOne({ where: { id } });
35 | if (!row) {
36 | const data = JSON.stringify({ role, content, createdAt: Date.now() });
37 | await MessageModel.bulkCreate([{
38 | id, chat_id, data
39 | }]);
40 | res.status(200).json(getMessageObj(id, chat_id, data));
41 | } else {
42 | row.data = JSON.stringify({
43 | ...JSON.parse(row.data),
44 | content,
45 | });
46 | await row.save();
47 | res.status(200).json(getMessageObj(id, chat_id, row.data));
48 | }
49 | } catch (error) {
50 | console.error(error);
51 | res.status(500).json({ error: "Internal Server Error" });
52 | }
53 | break;
54 | default:
55 | res.setHeader("Allow", ["GET", "POST"]);
56 | res.status(405).end(`Method ${method} Not Allowed`);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pages/api/socketio.js:
--------------------------------------------------------------------------------
1 | import { Server } from 'socket.io';
2 | import SocketIOClient from 'socket.io-client';
3 |
4 | export const config = {
5 | api: {
6 | bodyParser: false,
7 | },
8 | };
9 |
10 | const handler = async (req, res) => {
11 | if (res.socket.server.io) {
12 | console.log('Socket proxy is already running');
13 | } else {
14 | console.log('Socket proxy is initializing');
15 | const io = new Server(res.socket.server, {
16 | path: '/api/socketio'
17 | });
18 | res.socket.server.io = io;
19 |
20 | io.on('connection', socket => {
21 | const target = SocketIOClient.connect(`ws://127.0.0.1:${process.env.WS_PORT}`);
22 |
23 | socket.on('disconnect', () => {
24 | target.disconnect();
25 | });
26 |
27 | target.on('disconnect', () => {
28 | socket.disconnect();
29 | });
30 |
31 | socket.onAny((event, ...args) => {
32 | target.emit(event, ...args);
33 | });
34 |
35 | target.onAny((event, ...args) => {
36 | socket.emit(event, ...args);
37 | });
38 | });
39 | }
40 | res.end();
41 | };
42 |
43 | export default handler;
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Head from "next/head";
3 | import { Inter } from "@next/font/google";
4 | const inter = Inter({ subsets: ["latin"] });
5 | import ChatHistory from "../components/ChatHistory";
6 | import Chats from "../components/Chats";
7 | import { getAllChats } from "./api/chats";
8 | import { AppContextProvider, useAppContext } from "../utils/AppContext";
9 | import Dialog from "../components/Dialog";
10 |
11 | export default function Home({ prefetchedChats }) {
12 | return (
13 | <>
14 |
15 | Alpaca.cpp Web UI
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | >
24 | );
25 | }
26 |
27 | const Main = () => {
28 | const { error } = useAppContext();
29 |
30 | return (
31 |
32 | {error?.type === 'ERR_NO_MODEL' && }
33 | {error?.type === 'ERR_WS' && }
34 |
35 | {" "}
41 |
42 | );
43 | };
44 |
45 | export async function getServerSideProps(context) {
46 | return {
47 | props: {
48 | prefetchedChats: await getAllChats(),
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngxson/alpaca.cpp-webui/f1962eb10e303b7b4701cdc881633b279bfbc64a/public/favicon.ico
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | width: var(--max-width);
46 | max-width: 100%;
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo,
108 | .thirteen {
109 | position: relative;
110 | }
111 |
112 | .thirteen {
113 | display: flex;
114 | justify-content: center;
115 | align-items: center;
116 | width: 75px;
117 | height: 75px;
118 | padding: 25px 10px;
119 | margin-left: 16px;
120 | transform: translateZ(0);
121 | border-radius: var(--border-radius);
122 | overflow: hidden;
123 | box-shadow: 0px 2px 8px -1px #0000001a;
124 | }
125 |
126 | .thirteen::before,
127 | .thirteen::after {
128 | content: '';
129 | position: absolute;
130 | z-index: -1;
131 | }
132 |
133 | /* Conic Gradient Animation */
134 | .thirteen::before {
135 | animation: 6s rotate linear infinite;
136 | width: 200%;
137 | height: 200%;
138 | background: var(--tile-border);
139 | }
140 |
141 | /* Inner Square */
142 | .thirteen::after {
143 | inset: 0;
144 | padding: 1px;
145 | border-radius: var(--border-radius);
146 | background: linear-gradient(
147 | to bottom right,
148 | rgba(var(--tile-start-rgb), 1),
149 | rgba(var(--tile-end-rgb), 1)
150 | );
151 | background-clip: content-box;
152 | }
153 |
154 | /* Enable hover only on non-touch devices */
155 | @media (hover: hover) and (pointer: fine) {
156 | .card:hover {
157 | background: rgba(var(--card-rgb), 0.1);
158 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
159 | }
160 |
161 | .card:hover span {
162 | transform: translateX(4px);
163 | }
164 | }
165 |
166 | @media (prefers-reduced-motion) {
167 | .thirteen::before {
168 | animation: none;
169 | }
170 |
171 | .card:hover span {
172 | transform: none;
173 | }
174 | }
175 |
176 | /* Mobile */
177 | @media (max-width: 700px) {
178 | .content {
179 | padding: 4rem;
180 | }
181 |
182 | .grid {
183 | grid-template-columns: 1fr;
184 | margin-bottom: 120px;
185 | max-width: 320px;
186 | text-align: center;
187 | }
188 |
189 | .card {
190 | padding: 1rem 2.5rem;
191 | }
192 |
193 | .card h2 {
194 | margin-bottom: 0.5rem;
195 | }
196 |
197 | .center {
198 | padding: 8rem 0 6rem;
199 | }
200 |
201 | .center::before {
202 | transform: none;
203 | height: 300px;
204 | }
205 |
206 | .description {
207 | font-size: 0.8rem;
208 | }
209 |
210 | .description a {
211 | padding: 1rem;
212 | }
213 |
214 | .description p,
215 | .description div {
216 | display: flex;
217 | justify-content: center;
218 | position: fixed;
219 | width: 100%;
220 | }
221 |
222 | .description p {
223 | align-items: center;
224 | inset: 0 0 auto;
225 | padding: 2rem 1rem 1.4rem;
226 | border-radius: 0;
227 | border: none;
228 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
229 | background: linear-gradient(
230 | to bottom,
231 | rgba(var(--background-start-rgb), 1),
232 | rgba(var(--callout-rgb), 0.5)
233 | );
234 | background-clip: padding-box;
235 | backdrop-filter: blur(24px);
236 | }
237 |
238 | .description div {
239 | align-items: flex-end;
240 | pointer-events: none;
241 | inset: auto 0 0;
242 | padding: 2rem;
243 | height: 200px;
244 | background: linear-gradient(
245 | to bottom,
246 | transparent 0%,
247 | rgb(var(--background-end-rgb)) 40%
248 | );
249 | z-index: 1;
250 | }
251 | }
252 |
253 | /* Tablet and Smaller Desktop */
254 | @media (min-width: 701px) and (max-width: 1120px) {
255 | .grid {
256 | grid-template-columns: repeat(2, 50%);
257 | }
258 | }
259 |
260 | @media (prefers-color-scheme: dark) {
261 | .vercelLogo {
262 | filter: invert(1);
263 | }
264 |
265 | .logo,
266 | .thirteen img {
267 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
268 | }
269 | }
270 |
271 | @keyframes rotate {
272 | from {
273 | transform: rotate(360deg);
274 | }
275 | to {
276 | transform: rotate(0deg);
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | :root {
5 | --max-width: 1100px;
6 | --border-radius: 12px;
7 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
8 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
9 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
10 |
11 | --foreground-rgb: 0, 0, 0;
12 | --background-start-rgb: 214, 219, 220;
13 | --background-end-rgb: 255, 255, 255;
14 |
15 | --primary-glow: conic-gradient(
16 | from 180deg at 50% 50%,
17 | #16abff33 0deg,
18 | #0885ff33 55deg,
19 | #54d6ff33 120deg,
20 | #0071ff33 160deg,
21 | transparent 360deg
22 | );
23 | --secondary-glow: radial-gradient(
24 | rgba(255, 255, 255, 1),
25 | rgba(255, 255, 255, 0)
26 | );
27 |
28 | --tile-start-rgb: 239, 245, 249;
29 | --tile-end-rgb: 228, 232, 233;
30 | --tile-border: conic-gradient(
31 | #00000080,
32 | #00000040,
33 | #00000030,
34 | #00000020,
35 | #00000010,
36 | #00000010,
37 | #00000080
38 | );
39 |
40 | --callout-rgb: 238, 240, 241;
41 | --callout-border-rgb: 172, 175, 176;
42 | --card-rgb: 180, 185, 188;
43 | --card-border-rgb: 131, 134, 135;
44 | }
45 |
46 | @media (prefers-color-scheme: dark) {
47 | :root {
48 | --foreground-rgb: 255, 255, 255;
49 | --background-start-rgb: 0, 0, 0;
50 | --background-end-rgb: 0, 0, 0;
51 |
52 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
53 | --secondary-glow: linear-gradient(
54 | to bottom right,
55 | rgba(1, 65, 255, 0),
56 | rgba(1, 65, 255, 0),
57 | rgba(1, 65, 255, 0.3)
58 | );
59 |
60 | --tile-start-rgb: 2, 13, 46;
61 | --tile-end-rgb: 2, 5, 19;
62 | --tile-border: conic-gradient(
63 | #ffffff80,
64 | #ffffff40,
65 | #ffffff30,
66 | #ffffff20,
67 | #ffffff10,
68 | #ffffff10,
69 | #ffffff80
70 | );
71 |
72 | --callout-rgb: 20, 20, 20;
73 | --callout-border-rgb: 108, 108, 108;
74 | --card-rgb: 100, 100, 100;
75 | --card-border-rgb: 200, 200, 200;
76 | }
77 | }
78 |
79 | * {
80 | box-sizing: border-box;
81 | padding: 0;
82 | margin: 0;
83 | }
84 |
85 | html,
86 | body {
87 | max-width: 100vw;
88 | overflow-x: hidden;
89 | }
90 |
91 | body {
92 | color: rgb(var(--foreground-rgb));
93 | background: linear-gradient(
94 | to bottom,
95 | transparent,
96 | rgb(var(--background-end-rgb))
97 | )
98 | rgb(var(--background-start-rgb));
99 | }
100 |
101 | a {
102 | color: inherit;
103 | text-decoration: none;
104 | }
105 |
106 | @media (prefers-color-scheme: dark) {
107 | html {
108 | color-scheme: dark;
109 | }
110 | }
111 |
112 | .overflow-y-auto {
113 | overflow-y: auto;
114 | -webkit-overflow-scrolling: touch;
115 | }
116 |
117 | ::-webkit-scrollbar {
118 | width: 0.6rem;
119 | background-color: #f5f5f5;
120 | border-radius: 10px;
121 | }
122 |
123 | ::-webkit-scrollbar-thumb {
124 | background-color: #000000;
125 | border-radius: 10px;
126 | }
127 |
128 | ::-webkit-scrollbar-thumb:hover {
129 | background-color: #555555;
130 | }
131 |
132 | .chat-text {
133 | max-height: 200px;
134 | height: 24px;
135 | overflow-y: hidden;
136 | }
137 | .chat {
138 | z-index: 99;
139 | }
140 |
141 | .fade {
142 | z-index: 0;
143 | background-color: transparent;
144 | background-image: linear-gradient(
145 | 180deg,
146 | rgba(53, 55, 64, 0),
147 | rgba(243, 244, 246, 0.5) 10%,
148 | rgba(243, 244, 246, 0.5) 20%,
149 | rgba(243, 244, 246, 0.75) 40%,
150 | rgba(243, 244, 246, 1) 50%
151 | );
152 | }
153 |
154 | .bg-gray-1000 {
155 | background-color: rgba(32, 33, 35, 1);
156 | }
157 | .loading-icon-container {
158 | display: flex;
159 | justify-content: center;
160 | align-items: center;
161 | height: 100%;
162 | }
163 |
164 | .loading-icon {
165 | width: 1.25rem;
166 | height: 1.25rem;
167 | border-radius: 50%;
168 | border: 0.25rem solid rgb(252, 250, 250);
169 | border-top-color: #3498db;
170 | animation: spin 1s infinite linear;
171 | }
172 |
173 | @keyframes spin {
174 | to {
175 | transform: rotate(360deg);
176 | }
177 | }
178 |
179 |
180 |
181 | .bot_cursor {
182 | animation-duration: 2s;
183 | animation-name: bot_cursor_anim;
184 | animation-delay: 0;
185 | animation-iteration-count: infinite;
186 | animation-direction: forward;
187 | }
188 |
189 | @keyframes bot_cursor_anim {
190 | 0% { opacity: 1; }
191 | 50% { opacity: 1; }
192 | 51% { opacity: 0; }
193 | 90% { opacity: 0; }
194 | 91% { opacity: 1; }
195 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./app/**/*.{js,ts,jsx,tsx}",
5 | "./pages/**/*.{js,ts,jsx,tsx}",
6 | "./components/**/*.{js,ts,jsx,tsx}",
7 |
8 | // Or if using `src` directory:
9 | "./src/**/*.{js,ts,jsx,tsx}",
10 | ],
11 | theme: {
12 | extend: {
13 | maxHeight: {
14 | 120: "60vh",
15 | "100%": "100%",
16 | "30vh": "30vh",
17 | },
18 | minHeight: {
19 | "10vh": "12vh",
20 | },
21 | height: {
22 | "85%": "85%",
23 | "15%": "15%",
24 | },
25 | width: {
26 | "50vw": "50vw",
27 | "10vw": "10vw",
28 | "79.71%": "79.71%",
29 | "20.29%": "20.29%",
30 | },
31 | backgroundColor: {
32 | "gray-800": "rgba(52,53,65,1)",
33 | "vert-light-gradient": "linear-gradient(180deg,hsla(0,0%,100%,0) 13.94%,#fff 54.73%)",
34 | },
35 | backgroundImage:{
36 | "vert-light-gradient": "linear-gradient(180deg,hsla(0,0%,100%,0) 13.94%,#fff 54.73%)",
37 | }
38 | },
39 | },
40 | plugins: [],
41 | };
42 |
--------------------------------------------------------------------------------
/utils/AppContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
2 | import SocketIOClient from "socket.io-client";
3 |
4 | const AppContext = createContext({});
5 |
6 | export const AppContextProvider = ({ children, prefetchedChats, wsPort }) => {
7 | const [chats, setChats] = useState(prefetchedChats);
8 | const [selectedChat, setSelectedChat] = useState(null);
9 | const [userText, setUserText] = useState("");
10 | const [error, setError] = useState(null);
11 | const [assistantTypingMsgId, setAssistantTypingMsgId] = useState(null);
12 | const [socket, setSocket] = useState();
13 | const [userConfig, setUserConfig] = useState({});
14 | const [showMenu, setShowMenu] = useState(true);
15 |
16 | useEffect(() => {
17 | setError(null);
18 | }, [selectedChat]);
19 |
20 | useEffect(() => {
21 | if (typeof window !== "undefined") {
22 | const protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
23 | setSocket(
24 | SocketIOClient.connect(`${protocol}://${location.host}`, {
25 | path: '/api/socketio'
26 | })
27 | );
28 | }
29 | }, []);
30 |
31 | useEffect(() => {
32 | if (!socket) return;
33 |
34 | socket.on("connect", () => {
35 | console.log("SOCKET CONNECTED!", socket.id);
36 | setError(null);
37 | });
38 |
39 | socket.on("connect_error", () => setError({ type: 'ERR_WS' }));
40 | socket.on("disconnect", () => setError({ type: 'ERR_WS' }));
41 | socket.on("error_missing_file", ({ pathExecAbs, modelPathAbs }) => setError({
42 | type: 'ERR_NO_MODEL',
43 | pathExecAbs,
44 | modelPathAbs,
45 | }));
46 |
47 | socket.on("user_config", (cfg) => setUserConfig(cfg));
48 |
49 | socket.on("update", (data) => {
50 | console.log('update', data)
51 |
52 | if (data.done) {
53 | setAssistantTypingMsgId(null);
54 | return;
55 | }
56 |
57 | const { chatId, messageId, input, output } = data;
58 | setAssistantTypingMsgId(messageId);
59 | setChats(chats => {
60 | return chats.map(c => {
61 | if (c.id === chatId) {
62 | //console.log(c.messages)
63 | const message = c.messages.find(m => m.id === messageId);
64 | if (message) { // already exist
65 | const messages = c.messages.map(m => m.id === messageId ? {
66 | ...m,
67 | content: output,
68 | } : m)
69 | return {...c, messages};
70 | } else { // new message
71 | const newMessage = {
72 | id: messageId,
73 | role: 'assistant',
74 | content: output,
75 | createdAt: Date.now(),
76 | };
77 | return {...c, messages: [...c.messages, newMessage]};
78 | }
79 | } else {
80 | return c;
81 | }
82 | })
83 | });
84 | });
85 |
86 | // socket disconnet onUnmount if exists
87 | if (socket) return () => socket.disconnect();
88 | // eslint-disable-next-line
89 | }, [socket]);
90 |
91 | return ;
110 | };
111 |
112 | export const useAppContext = () => useContext(AppContext);
113 | export const withAppContext = Component => props => (
114 |
115 | {store => }
116 |
117 | );
--------------------------------------------------------------------------------
/utils/conversation.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | // inspiration: https://colab.research.google.com/drive/115ba3EFCT0PvyXzFNv9E18QnKiyyjsm5?usp=sharing
4 |
5 | const CONVERSATION_TEMPLATE = ({ prompt, history, input }) => `${prompt.trim()}
6 |
7 | Conversation:
8 | ${history}
9 | Human: ${input}
10 | AI:`;
11 |
12 | export const getConversationPrompt = (messages, input, historyLength, prompt) => {
13 | const history = _.takeRight(messages, parseInt(historyLength)).map(m =>
14 | `${m.role === 'user' ? 'Human' : 'AI'}: ${m.content || '(say nothing)'}`
15 | ).join('\n');
16 | return CONVERSATION_TEMPLATE({ prompt, history, input });
17 | };
18 |
--------------------------------------------------------------------------------
/utils/db.js:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | const getDataDir = () => {
6 | let dir = __dirname;
7 | if (__dirname.match(/\.next/)) {
8 | dir = dir.replace(/\.next.*/, '');
9 | } else {
10 | dir = dir.replace(/utils(\\|\/)/, '');
11 | }
12 | dir = path.join(dir, '.data');
13 | if (!fs.existsSync(dir)) fs.mkdirSync(dir);
14 | return dir;
15 | };
16 |
17 | const dbDir = path.join(getDataDir(), 'db.sqlite');
18 | console.log('load sqlite from', dbDir);
19 |
20 | /** @type {Sequelize} */
21 | const sequelize = new Sequelize({
22 | dialect: 'sqlite',
23 | storage: dbDir,
24 | });
25 |
26 |
27 | //////////////////////////////////////////////////////
28 |
29 | /** @type {Sequelize.Model} */
30 | export const MessageModel = sequelize.define('message', {
31 | id: { type: Sequelize.STRING, allowNull: false, primaryKey: true },
32 | chat_id: { type: Sequelize.STRING, allowNull: false },
33 | data: { type: Sequelize.STRING, allowNull: false },
34 | }, {
35 | timestamps: false
36 | });
37 |
38 | (async () => {
39 | await MessageModel.sync();
40 | const query = 'CREATE INDEX IF NOT EXISTS message_chat_id_idx ON messages(chat_id)';
41 | await sequelize.query(query);
42 | })();
43 |
44 | //////////////////////////////////////////////////////
45 |
46 | /** @type {Sequelize.Model} */
47 | export const ChatModel = sequelize.define('chat', {
48 | id: { type: Sequelize.STRING, allowNull: false, primaryKey: true },
49 | title: { type: Sequelize.STRING },
50 | data: { type: Sequelize.STRING },
51 | }, {
52 | timestamps: false
53 | });
54 |
55 | (async () => {
56 | await ChatModel.sync();
57 | })();
58 |
--------------------------------------------------------------------------------
/utils/lang-detector.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The MIT License (MIT)
3 | *
4 | * Copyright (c) 2015 Toni Sučić
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 |
25 | var _ = require('underscore');
26 |
27 | /**
28 | * A checker is an object with the following form:
29 | * { pattern: /something/, points: 1 }
30 | * or if the pattern only matches code near the top of a given file:
31 | * { pattern: /something/, points: 2, nearTop: true }
32 | *
33 | * Key: Language name.
34 | * Value: Array of checkers.
35 | *
36 | * N.B. An array of checkers shouldn't contain more regexes than
37 | * necessary as it would inhibit performance.
38 | *
39 | * Points scale:
40 | * 2 = Bonus points: Almost unique to a given language.
41 | * 1 = Regular point: Not unique to a given language.
42 | * -1 = Penalty point: Does not match a given language.
43 | * Rare:
44 | * -50 = Bonus penalty points: Only used when two languages are mixed together,
45 | * and one has a higher precedence over the other one.
46 | */
47 | var languages = {
48 | 'JavaScript': [
49 | // undefined keyword
50 | { pattern: /undefined/g, points: 2 },
51 | // console.log('ayy lmao')
52 | { pattern: /console\.log( )*\(/, points: 2 },
53 | // Variable declaration
54 | { pattern: /(var|const|let)( )+\w+( )*=?/, points: 2 },
55 | // Array/Object declaration
56 | { pattern: /(('|").+('|")( )*|\w+):( )*[{\[]/, points: 2 },
57 | // === operator
58 | { pattern: /===/g, points: 1 },
59 | // !== operator
60 | { pattern: /!==/g, points: 1 },
61 | // Function definition
62 | { pattern: /function\*?(( )+[\$\w]+( )*\(.*\)|( )*\(.*\))/g, points: 1 },
63 | // null keyword
64 | { pattern: /null/g, points: 1 },
65 | // lambda expression
66 | { pattern: /\(.*\)( )*=>( )*.+/, points: 1 },
67 | // (else )if statement
68 | { pattern: /(else )?if( )+\(.+\)/, points: 1 },
69 | // while loop
70 | { pattern: /while( )+\(.+\)/, points: 1 },
71 | // C style variable declaration.
72 | { pattern: /(^|\s)(char|long|int|float|double)( )+\w+( )*=?/, points: -1 },
73 | // pointer
74 | { pattern: /(\w+)( )*\*( )*\w+/, points: -1 },
75 | // HTML