├── .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 | ![](./doc/screenshot_0.png) 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 | 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 | 116 |
} 117 | 118 | {error &&
119 | 120 |
} 121 | 122 |
e.preventDefault()} 126 | style={{boxShadow:"0 0 20px 0 rgba(0, 0, 0, 0.1)"}} 127 | > 128 |
129 | 151 |
152 | 153 | 177 |
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 | 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 | 129 | 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 | 187 |
188 |
189 | {type === 'input' && } 190 | {type === 'textarea' &&