├── .eslintrc.json ├── .github └── image.png ├── .gitignore ├── LICENSE ├── README.md ├── components ├── Button.tsx ├── ChannelTop.tsx ├── ChatBar.tsx ├── Image.tsx ├── Message.tsx ├── MessageList.tsx ├── PromptBook.tsx ├── PromptEngine.tsx └── Settings.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx └── index.tsx ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJdev/diffusion-chat/cc213f1b760f216d5c5a979de357e3acb51bdc36/.github/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /api 6 | /.pnp 7 | .pnp.js 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 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | .env 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ezekiel Wotring 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Diffusion Chat 3 | The future of human-AI interaction is conversation. Inference is iterative. Diffusion Chat allows you to speak directly to Stable Diffusion. Built in nextjs (edge runtime), tailwindcss, zustand. Communicating with the Stability AI REST API. 4 | 5 | Live version of the site at https://diffusion.chat 6 | 7 | ![](https://github.com/KAJdev/diffusion-chat/blob/main/.github/image.png) 8 | 9 | ## Contributing 10 | Just make a PR and try to follow the functional paradigm. All skill levels welcome. Let's build AI interactions together :). 11 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from "./Message"; 2 | import { PromptBook } from "./PromptBook"; 3 | 4 | function saveImage(image: string, name: string) { 5 | // download image from external URL 6 | fetch(image) 7 | .then((res) => res.blob()) 8 | .then((blob) => { 9 | // create blob link to download 10 | const url = window.URL.createObjectURL(new Blob([blob])); 11 | const link = document.createElement("a"); 12 | link.setAttribute("download", `${name}.png`); 13 | link.setAttribute("href", url); 14 | document.body.appendChild(link); 15 | link.click(); 16 | link.remove(); 17 | }); 18 | } 19 | 20 | export function Button({ 21 | btn, 22 | message, 23 | selectedImage, 24 | }: { 25 | btn: Button; 26 | message: Message; 27 | selectedImage: number; 28 | }) { 29 | const addPrompt = PromptBook.use((state) => state.addPrompt); 30 | 31 | return ( 32 | 60 | ); 61 | } 62 | 63 | export type Button = { 64 | text: string; 65 | id: string; 66 | }; 67 | -------------------------------------------------------------------------------- /components/ChannelTop.tsx: -------------------------------------------------------------------------------- 1 | import { MessageCircle } from "lucide-react"; 2 | 3 | export function ChannelTop() { 4 | return ( 5 | <> 6 |
7 |
8 | 9 |
10 |

Welcome to #diffusion-chat

11 |

12 | Talk directly to latent space. 13 |

14 |
15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/ChatBar.tsx: -------------------------------------------------------------------------------- 1 | import { Album, Send, Settings2 } from "lucide-react"; 2 | import create from "zustand"; 3 | import { Message } from "./Message"; 4 | import { MessageList } from "./MessageList"; 5 | import { PromptBook } from "./PromptBook"; 6 | import { PromptEngine } from "./PromptEngine"; 7 | import { Settings } from "./Settings"; 8 | 9 | export function ChatBar() { 10 | const [prompt, setPrompt] = ChatBar.use((state) => [ 11 | state.prompt, 12 | state.setPrompt, 13 | ]); 14 | 15 | const [promptBookOpen, setPromptBookOpen] = PromptBook.use((state) => [ 16 | state.isOpen, 17 | state.setOpen, 18 | ]); 19 | 20 | const [settingsOpen, setSettingsOpen] = Settings.use((state) => [ 21 | state.isOpen, 22 | state.setOpen, 23 | ]); 24 | 25 | const history = MessageList.use((state) => state.messages); 26 | 27 | return ( 28 | <> 29 |
34 |
41 |
42 | {"Don't know what to say? "}{" "} 43 | setPrompt(PromptEngine.makePrompt())} 46 | > 47 | Surprise Me! 48 | 49 |
50 |
51 |
52 |
53 | 54 | 55 |
62 | setPrompt(e.target.value)} 68 | onKeyDown={(e) => { 69 | if (e.key === "Enter") { 70 | Message.sendPromptMessage(prompt); 71 | e.preventDefault(); 72 | // check if on mobile 73 | if (window.innerWidth < 768) { 74 | // @ts-ignore 75 | e.target.blur(); 76 | } 77 | } else if ( 78 | e.key === "ArrowUp" && 79 | !prompt && 80 | Object.keys(history).length > 0 81 | ) { 82 | setPrompt( 83 | Object.values(history).sort( 84 | (a, b) => b.timestamp - a.timestamp 85 | )[0].prompt 86 | ); 87 | 88 | e.preventDefault(); 89 | } 90 | }} 91 | style={{ 92 | background: "transparent", 93 | padding: "0", 94 | border: "none", 95 | outline: "none", 96 | }} 97 | autoFocus 98 | /> 99 |
100 | 116 | 130 |
131 | 143 |
144 |
145 |
146 | 147 | ); 148 | } 149 | 150 | export type ChatBar = { 151 | prompt: string; 152 | setPrompt: (prompt: string) => void; 153 | }; 154 | 155 | export namespace ChatBar { 156 | export const use = create()((set) => ({ 157 | prompt: "", 158 | setPrompt: (prompt: string) => set((state: ChatBar) => ({ prompt })), 159 | })); 160 | } 161 | -------------------------------------------------------------------------------- /components/Image.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Artifact, Message } from "./Message"; 3 | 4 | export function Image({ 5 | image, 6 | selectedImage, 7 | setSelectedImage, 8 | message, 9 | i, 10 | }: { 11 | image: Artifact; 12 | selectedImage: number; 13 | setSelectedImage(i: number): void; 14 | message: Message; 15 | i: number; 16 | }) { 17 | const [loaded, setLoaded] = React.useState(false); 18 | 19 | return ( 20 | // eslint-disable-next-line @next/next/no-img-element 21 | Generated image -1 && 29 | selectedImage !== i && 30 | message.images?.length !== 1 31 | ? "-mx-1" 32 | : "" 33 | }`} 34 | style={{ 35 | maxHeight: 36 | selectedImage === i || message.images?.length === 1 37 | ? "25rem" 38 | : "10rem", 39 | maxWidth: 40 | selectedImage === i || message.images?.length === 1 41 | ? "25rem" 42 | : "10rem", 43 | height: 44 | selectedImage > -1 && 45 | selectedImage !== i && 46 | message.images?.length !== 1 47 | ? "0" 48 | : `${message.settings?.height}px`, 49 | width: 50 | selectedImage > -1 && 51 | selectedImage !== i && 52 | message.images?.length !== 1 53 | ? "0" 54 | : `${message.settings?.width}px`, 55 | }} 56 | onClick={() => { 57 | if (message.images?.length === 1) return; 58 | 59 | if (selectedImage === i) { 60 | setSelectedImage(-1); 61 | } else { 62 | setSelectedImage(i); 63 | 64 | if ( 65 | window.innerHeight + document.documentElement.scrollTop === 66 | document.documentElement.offsetHeight 67 | ) { 68 | setTimeout(() => { 69 | window.scrollTo({ 70 | behavior: "smooth", 71 | top: document.body.scrollHeight, 72 | }); 73 | }, 300); 74 | } 75 | } 76 | }} 77 | onLoad={() => { 78 | window.scrollTo({ 79 | behavior: "smooth", 80 | top: document.body.scrollHeight, 81 | }); 82 | setLoaded(true); 83 | }} 84 | /> 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /components/Message.tsx: -------------------------------------------------------------------------------- 1 | import { Frown, Smile, Wand2 } from "lucide-react"; 2 | import React from "react"; 3 | import { Button } from "./Button"; 4 | import { ChatBar } from "./ChatBar"; 5 | import { Image } from "./Image"; 6 | import { MessageList, sessionID, makeId } from "./MessageList"; 7 | import { PromptBook } from "./PromptBook"; 8 | import { PromptEngine } from "./PromptEngine"; 9 | import { Settings } from "./Settings"; 10 | 11 | export function Message({ id }: { id: string }) { 12 | const [message, editMessage] = MessageList.useMessage(id); 13 | const [selectedImage, setSelectedImage] = React.useState(-1); 14 | 15 | const savedPrompts = PromptBook.use((state) => state.prompts); 16 | 17 | return ( 18 |
19 |
24 | {message.images && message.images.length > 0 && ( 25 |
26 |
Message.rateMessage(message, 1)} 28 | className={`p-1.5 border-r border-[#31363f] last-of-type:border-transparent duration-200 cursor-pointer ${ 29 | message.rating < 3 30 | ? "bg-white/[7%] text-red-300" 31 | : "text-red-300/30 hover:text-red-300 hover:bg-white/5" 32 | }`} 33 | > 34 | 35 |
36 |
Message.rateMessage(message, 5)} 38 | className={`p-1.5 border-r border-[#31363f] last-of-type:border-transparent duration-200 cursor-pointer ${ 39 | message.rating > 3 40 | ? "bg-white/[7%] text-green-300" 41 | : "text-green-300/30 hover:text-green-300 hover:bg-white/5" 42 | }`} 43 | > 44 | 45 |
46 |
47 | )} 48 | {message.rating !== 3 && ( 49 |
50 | {message.rating < 3 && ( 51 | 52 | )} 53 | {message.rating > 3 && ( 54 | 55 | )} 56 |
57 | )} 58 |
59 |

60 | {message.type === "you" ? "You" : "Stable Diffusion"} 61 |

62 | {message.timestamp && ( 63 |

64 | {new Date(message.timestamp).toLocaleTimeString()} 65 |

66 | )} 67 | {message.modifiers && ( 68 | 69 | )} 70 |
71 | {message.prompt && message.type === "you" && ( 72 |

{message.prompt}

73 | )} 74 | {message.images && message.settings && message.images.length > 0 && ( 75 |
78 | {message.images.map((image, i) => ( 79 | // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text 80 | 88 | ))} 89 |
90 | )} 91 | {message.error &&

{message.error}

} 92 | {message.loading && message.images && message.images.length === 0 && ( 93 |
94 |
95 |
96 |
97 |
98 | )} 99 | {message.buttons && message.buttons.length > 0 && ( 100 |
101 | {message.buttons.map((btn, i) => { 102 | if ( 103 | btn.id === "save_prompt" && 104 | (!message.prompt || savedPrompts.includes(message.prompt)) 105 | ) 106 | return null; 107 | 108 | return ( 109 |
118 | )} 119 |
120 |
121 | ); 122 | } 123 | 124 | export enum MessageType { 125 | YOU = "you", 126 | STABLE_DIFFUSION = "stable diffusion", 127 | OTHER = "other", 128 | SYSTEM = "system", 129 | } 130 | 131 | export type Message = { 132 | type: MessageType; 133 | id: string; 134 | timestamp: number; 135 | prompt: string; 136 | modifiers: string | undefined; 137 | loading: boolean; 138 | buttons: Button[]; 139 | error: string | null; 140 | images: Artifact[]; 141 | settings: Settings | null; 142 | rating: number; 143 | }; 144 | 145 | export type Artifact = { 146 | image: string; 147 | seed: number; 148 | id: string; 149 | }; 150 | 151 | const SURE_ANIME_WORDS = [ 152 | "1girl", 153 | "2girls", 154 | "highres", 155 | "looking at viewer", 156 | "looking_at_viewer", 157 | ]; 158 | 159 | const POSSIBLE_ANIME_WORDS = [ 160 | "breasts", 161 | "skirt", 162 | "blush", 163 | "smile", 164 | "solo", 165 | "simple background", 166 | "simple_background", 167 | "multiple girls", 168 | "multiple_girls", 169 | ]; 170 | 171 | export namespace Message { 172 | export const b64toBlob = (b64Data: string, contentType = "") => { 173 | // Decode the base64 string into a new Buffer object 174 | const buffer = Buffer.from(b64Data, "base64"); 175 | 176 | // Create a new blob object from the buffer 177 | return new Blob([buffer], { type: contentType }); 178 | }; 179 | 180 | export const rateMessage = async (message: Message, rating: number) => { 181 | MessageList.use.setState((state) => { 182 | const newMessages = { ...state.messages }; 183 | newMessages[message.id].rating = rating; 184 | return { ...state, messages: newMessages }; 185 | }); 186 | 187 | let res = null; 188 | try { 189 | res = await fetch("https://api.diffusion.chat/rate", { 190 | method: "POST", 191 | headers: { "Content-Type": "application/json" }, 192 | body: JSON.stringify({ 193 | ids: message.images.map((img) => img.id), 194 | rating, 195 | }), 196 | }); 197 | } catch (e) { 198 | console.log(e); 199 | } 200 | 201 | if (!res || !res.ok) { 202 | MessageList.use.setState((state) => { 203 | const newMessages = { ...state.messages }; 204 | newMessages[message.id].rating = 3; 205 | return { ...state, messages: newMessages }; 206 | }); 207 | } else { 208 | console.log("rated", res); 209 | } 210 | }; 211 | 212 | export const sendPromptMessage = async ( 213 | prompt: string, 214 | modifiers?: string 215 | ) => { 216 | if (!prompt && !modifiers) return; 217 | 218 | const settings = Settings.use.getState().settings; 219 | Settings.use.getState().setOpen(false); 220 | 221 | if (prompt.length < 150 && !modifiers) { 222 | modifiers = PromptEngine.getModifers(); 223 | } 224 | 225 | if (prompt.length < 35) { 226 | if (modifiers) { 227 | modifiers = prompt + " " + modifiers; 228 | } else { 229 | modifiers = prompt; 230 | } 231 | } 232 | 233 | if (!settings.modify) { 234 | modifiers = undefined; 235 | } 236 | 237 | ChatBar.use.getState().setPrompt(""); 238 | 239 | const uid = makeId(); 240 | const newMsg: Message = { 241 | type: MessageType.YOU, 242 | id: uid, 243 | prompt: prompt, 244 | modifiers: modifiers || undefined, 245 | timestamp: Date.now(), 246 | loading: true, 247 | buttons: [], 248 | error: null, 249 | images: [], 250 | settings: settings, 251 | rating: 3, 252 | }; 253 | MessageList.use.getState().addMessage(newMsg); 254 | 255 | let res = null; 256 | 257 | try { 258 | if ( 259 | settings.model == "anything-v3.0" || 260 | SURE_ANIME_WORDS.some((word) => prompt.includes(word)) || 261 | POSSIBLE_ANIME_WORDS.filter((word) => prompt.includes(word)).length >= 3 262 | ) { 263 | res = await fetch("https://api.diffusion.chat/anime", { 264 | method: "POST", 265 | headers: { "Content-Type": "application/json" }, 266 | body: JSON.stringify({ 267 | prompt: prompt, 268 | width: settings.width, 269 | height: settings.height, 270 | count: 4, 271 | steps: settings.steps, 272 | scale: settings.scale, 273 | session: sessionID, 274 | }), 275 | }); 276 | } else { 277 | res = await fetch("https://api.diffusion.chat/image", { 278 | method: "POST", 279 | headers: { "Content-Type": "application/json" }, 280 | body: JSON.stringify({ 281 | prompt: prompt, 282 | modifiers, 283 | model: settings.model, 284 | width: settings.width, 285 | height: settings.height, 286 | count: settings.count, 287 | steps: settings.steps, 288 | scale: settings.scale, 289 | session: sessionID, 290 | }), 291 | }); 292 | } 293 | } catch (e) { 294 | console.log(e); 295 | } 296 | 297 | if (!res || !res.ok) { 298 | switch (res?.status) { 299 | case 400: 300 | newMsg.error = "Bad request"; 301 | break; 302 | case 429: 303 | newMsg.error = "You're too fast! Slow down!"; 304 | break; 305 | default: 306 | newMsg.error = "Something went wrong"; 307 | break; 308 | } 309 | newMsg.loading = false; 310 | newMsg.buttons = [ 311 | { 312 | text: "Retry", 313 | id: "regenerate", 314 | }, 315 | ]; 316 | MessageList.use.getState().editMessage(uid, newMsg); 317 | return; 318 | } 319 | 320 | const data = await res.json(); 321 | newMsg.images = data; 322 | 323 | if (data.length == 0) { 324 | newMsg.error = "No results"; 325 | newMsg.loading = false; 326 | newMsg.buttons = [ 327 | { 328 | text: "Retry", 329 | id: "regenerate", 330 | }, 331 | ]; 332 | MessageList.use.getState().editMessage(uid, newMsg); 333 | return; 334 | } 335 | 336 | newMsg.loading = false; 337 | newMsg.buttons = [ 338 | { 339 | text: "Regenerate", 340 | id: "regenerate", 341 | }, 342 | { 343 | text: "Download", 344 | id: "save", 345 | }, 346 | { 347 | text: "Save Prompt", 348 | id: "save_prompt", 349 | }, 350 | ]; 351 | 352 | if (newMsg.modifiers) { 353 | newMsg.buttons.push({ 354 | text: "Remix", 355 | id: "remix", 356 | }); 357 | } 358 | 359 | console.log("new msg", newMsg); 360 | MessageList.use.getState().editMessage(uid, newMsg); 361 | }; 362 | } 363 | -------------------------------------------------------------------------------- /components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from "./Message"; 2 | import create from "zustand"; 3 | 4 | export const makeId = () => { 5 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 6 | var r = (Math.random() * 16) | 0, 7 | v = c == "x" ? r : (r & 0x3) | 0x8; 8 | return v.toString(16); 9 | }); 10 | }; 11 | 12 | export const sessionID = makeId(); 13 | 14 | export function MessageList() { 15 | const history = MessageList.use((state) => state.messages); 16 | 17 | return ( 18 | <> 19 | {Object.values(history).map((message) => ( 20 | <>{message && } 21 | ))} 22 | 23 | ); 24 | } 25 | 26 | export type MessageHistory = { 27 | messages: Record; 28 | addMessage: (message: Message) => void; 29 | editMessage: (id: string, message: Message) => void; 30 | deleteMessage: (id: string) => void; 31 | }; 32 | 33 | export namespace MessageList { 34 | export const use = create()((set) => ({ 35 | messages: {}, 36 | addMessage: (message: Message) => 37 | set((state: MessageHistory) => ({ 38 | messages: { ...state.messages, [message.id]: message }, 39 | })), 40 | editMessage: (id: string, message: Message) => 41 | set((state: MessageHistory) => ({ 42 | messages: { ...state.messages, [id]: message }, 43 | })), 44 | deleteMessage: (id: string) => { 45 | const messages = { ...MessageList.use.getState().messages }; 46 | delete messages[id]; 47 | set((state: MessageHistory) => ({ 48 | messages, 49 | })); 50 | }, 51 | })); 52 | 53 | export const useMessage = (id: string) => { 54 | const message = MessageList.use((state) => state.messages[id]); 55 | const setMessage = (message: Message) => 56 | MessageList.use.getState().editMessage(id, message); 57 | return [message, setMessage] as const; 58 | }; 59 | 60 | export const getLastNMessages = (n: number) => { 61 | const messages = use.getState().messages; 62 | 63 | const values = Object.values(messages); 64 | return values.sort((a, b) => b.timestamp - a.timestamp).slice(0, n); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /components/PromptBook.tsx: -------------------------------------------------------------------------------- 1 | import { Plus, Trash2, X } from "lucide-react"; 2 | import React from "react"; 3 | import create from "zustand"; 4 | import { ChatBar } from "./ChatBar"; 5 | 6 | export function PromptBook() { 7 | const [prompts, addPrompt, deletePrompt, setPrompts, open, setOpen] = 8 | PromptBook.use((state) => [ 9 | state.prompts, 10 | state.addPrompt, 11 | state.deletePrompt, 12 | state.setPrompts, 13 | state.isOpen, 14 | state.setOpen, 15 | ]); 16 | 17 | const [prompt, setPrompt] = ChatBar.use((state) => [ 18 | state.prompt, 19 | state.setPrompt, 20 | ]); 21 | 22 | React.useEffect(() => { 23 | // load from local storage 24 | const loadedPrompts = localStorage.getItem("prompts"); 25 | if (loadedPrompts?.startsWith("[") && loadedPrompts?.endsWith("]")) { 26 | setPrompts(JSON.parse(loadedPrompts)); 27 | } 28 | 29 | // write serializer 30 | const unsub = PromptBook.use.subscribe((newPrompts) => { 31 | localStorage.setItem("prompts", JSON.stringify(newPrompts.prompts)); 32 | }); 33 | 34 | return () => unsub(); 35 | }, [setPrompts]); 36 | 37 | return ( 38 |
45 | {prompts.length > 0 ? ( 46 | <> 47 | {prompt && !prompts.includes(prompt) && ( 48 | 58 | )} 59 | {prompts.map((prompt, i) => ( 60 | 81 | ))} 82 | 83 | ) : ( 84 |
85 | {prompt ? ( 86 | 92 | ) : ( 93 |

No prompts saved

94 | )} 95 |
96 | )} 97 |
98 | ); 99 | } 100 | 101 | export type PromptBook = { 102 | prompts: string[]; 103 | addPrompt: (prompt: string) => void; 104 | deletePrompt: (prompt: string) => void; 105 | setPrompts: (prompts: string[]) => void; 106 | 107 | isOpen: boolean; 108 | setOpen: (isOpen: boolean) => void; 109 | }; 110 | 111 | export namespace PromptBook { 112 | export const use = create()((set) => ({ 113 | prompts: [], 114 | addPrompt: (prompt: string) => 115 | set((state: PromptBook) => ({ 116 | prompts: [...state.prompts, prompt], 117 | })), 118 | deletePrompt: (prompt: string) => 119 | set((state: PromptBook) => ({ 120 | prompts: state.prompts.filter((p) => p !== prompt), 121 | })), 122 | setPrompts: (prompts: string[]) => 123 | set((state: PromptBook) => ({ prompts })), 124 | 125 | isOpen: false, 126 | setOpen: (isOpen: boolean) => set((state: PromptBook) => ({ isOpen })), 127 | })); 128 | } 129 | -------------------------------------------------------------------------------- /components/PromptEngine.tsx: -------------------------------------------------------------------------------- 1 | export namespace PromptEngine { 2 | export const makePrompt = () => { 3 | const template = templates[Math.floor(Math.random() * templates.length)]; 4 | let s = template 5 | .replace(/{noun}/g, nouns[Math.floor(Math.random() * nouns.length)]) 6 | .replace(/{gerund}/g, gerunds[Math.floor(Math.random() * gerunds.length)]) 7 | .replace( 8 | /{adjective}/g, 9 | adjectives[Math.floor(Math.random() * adjectives.length)] 10 | ) 11 | .replace(/{adverb}/g, adverbs[Math.floor(Math.random() * adverbs.length)]) 12 | .replace( 13 | /{artist}/g, 14 | artists[Math.floor(Math.random() * artists.length)] 15 | ); 16 | 17 | for (let i = 0; i < Math.random() * 10; i++) { 18 | s += ", " + modifiers[Math.floor(Math.random() * modifiers.length)]; 19 | } 20 | 21 | return s; 22 | }; 23 | 24 | export const getModifers = () => { 25 | const modifierArray = []; 26 | for (let i = 0; i < 10; i++) { 27 | modifierArray.push( 28 | modifiers[Math.floor(Math.random() * modifiers.length)] 29 | ); 30 | } 31 | return modifierArray.join(", "); 32 | }; 33 | 34 | const templates = [ 35 | "a {noun} {gerund} by {artist}", 36 | "{adjective} {noun} {gerund} by {artist}", 37 | "{adjective} {noun} {gerund} {adverb} by {artist}", 38 | "a {adjective} {noun} {gerund} {adverb} by {artist}", 39 | ]; 40 | 41 | const nouns = [ 42 | "dog", 43 | "cat", 44 | "bird", 45 | "fish", 46 | "person", 47 | "wizard", 48 | "witch", 49 | "dragon", 50 | "unicorn", 51 | "robot", 52 | "alien", 53 | "monster", 54 | "goblin", 55 | "elf", 56 | "dwarf", 57 | "orc", 58 | "troll", 59 | "giant", 60 | "golem", 61 | "demon", 62 | "angel", 63 | "ghost", 64 | "vampire", 65 | "werewolf", 66 | "zombie", 67 | "skeleton", 68 | ]; 69 | 70 | const gerunds = [ 71 | "running", 72 | "walking", 73 | "flying", 74 | "swimming", 75 | "singing", 76 | "dancing", 77 | "playing", 78 | "fighting", 79 | "hiding", 80 | "eating", 81 | "sleeping", 82 | "drinking", 83 | "smoking", 84 | "crying", 85 | "laughing", 86 | "screaming", 87 | "yelling", 88 | "reading", 89 | "writing", 90 | "drawing", 91 | "painting", 92 | "jumping", 93 | "hopping", 94 | "skipping", 95 | ]; 96 | 97 | const adjectives = [ 98 | "happy", 99 | "sad", 100 | "angry", 101 | "scared", 102 | "confused", 103 | "confident", 104 | "crazy", 105 | "silly", 106 | "funny", 107 | "weird", 108 | "strange", 109 | "odd", 110 | "boring", 111 | "exciting", 112 | "amazing", 113 | "beautiful", 114 | "ugly", 115 | "cute", 116 | "adorable", 117 | "handsome", 118 | "pretty", 119 | "smart", 120 | "dumb", 121 | "stupid", 122 | "clever", 123 | "brave", 124 | "shy", 125 | "quiet", 126 | "loud", 127 | "fast", 128 | "slow", 129 | "strong", 130 | "weak", 131 | "tall", 132 | "short", 133 | "fat", 134 | "skinny", 135 | ]; 136 | 137 | const adverbs = [ 138 | "quickly", 139 | "slowly", 140 | "happily", 141 | "sadly", 142 | "angrily", 143 | "scaredly", 144 | "confusedly", 145 | "confidently", 146 | "crazily", 147 | "sillyly", 148 | "funnily", 149 | "weirdly", 150 | "strangely", 151 | "oddly", 152 | "boringly", 153 | "excitingly", 154 | "amazingly", 155 | "beautifully", 156 | "cutely", 157 | "adorably", 158 | "handsomely", 159 | "prettyly", 160 | "smartly", 161 | ]; 162 | 163 | const artists = [ 164 | "A-1 Pictures", 165 | "Alvar Aalto", 166 | "Annie Leibovitz", 167 | "Antoni Gaudí", 168 | "Antonio Gaudí", 169 | "Antonio López García", 170 | "Ansel Adams", 171 | "Ansel Easton Adams", 172 | "Tyler Edlin", 173 | "Temmie Chang", 174 | "Terry Gilliam", 175 | "Terry Pratchett", 176 | "Terry Pratchett and Neil Gaiman", 177 | "Terry Pratchett and Stephen Baxter", 178 | "Ian Mcewan", 179 | ]; 180 | 181 | const modifiers = [ 182 | "acrylic painting", 183 | "trending on CGSociety", 184 | "trending on DeviantArt", 185 | "trending on ArtStation", 186 | "majestic", 187 | "epic", 188 | "legendary", 189 | "Maya", 190 | "8K", 191 | "4K", 192 | "wallpaper", 193 | "intricately detailed", 194 | "dramatic", 195 | "WLOP", 196 | "artgerm", 197 | "highly detailed", 198 | "by Greg Manchess", 199 | "by Antonio Moro", 200 | "Studio Ghibli", 201 | "Makoto Shinkai", 202 | "illustration", 203 | "digital painting", 204 | "concept art", 205 | "abstract art", 206 | "bloomcore", 207 | "rosepunk", 208 | "rosecore", 209 | "digital art", 210 | "digital painting", 211 | "digital illustration", 212 | "digital drawing", 213 | "digital sketch", 214 | "greg rutkowski", 215 | "masterpiece", 216 | "masterpiece by Greg Rutkowski", 217 | "fantasy", 218 | "fantasy art", 219 | "fantasy illustration", 220 | "fantasy painting", 221 | "fantasy drawing", 222 | "sharp focus", 223 | "Alphonse Mucha", 224 | "sharp", 225 | "Moebius", 226 | "Cyril Rolando", 227 | "Judy Chicago", 228 | "Blizzard", 229 | "elegant", 230 | "steampunk", 231 | "steampunk art", 232 | "steampunk illustration", 233 | ]; 234 | } 235 | -------------------------------------------------------------------------------- /components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import { Check } from "lucide-react"; 3 | import create from "zustand"; 4 | 5 | export function Settings() { 6 | const [open, settings, setSettings] = Settings.use((state) => [ 7 | state.isOpen, 8 | state.settings, 9 | state.setSettings, 10 | ]); 11 | 12 | return ( 13 |
20 |
21 |
22 |

Model

23 |
24 |
25 | {["1.5", "2.1", "2.1 large", "anime"].map((model) => ( 26 | 80 | ))} 81 |
82 |
83 | 84 |
85 |
86 |

Size

87 |

88 | {settings.width}x{settings.height} 89 |

90 |
91 | { 99 | setSettings({ 100 | ...settings, 101 | width: parseInt(e.target.value), 102 | height: parseInt(e.target.value), 103 | }); 104 | }} 105 | /> 106 |
107 | 108 |
109 |
110 |

Image Count

111 |

{settings.count}

112 |
113 | { 121 | setSettings({ 122 | ...settings, 123 | count: parseInt(e.target.value), 124 | }); 125 | }} 126 | /> 127 |
128 | 129 |
130 |
131 |

Steps

132 |

{settings.steps}

133 |
134 | { 142 | setSettings({ 143 | ...settings, 144 | steps: parseInt(e.target.value), 145 | }); 146 | }} 147 | /> 148 |
149 | 150 |
151 |
152 |

CFG Scale

153 |

{settings.scale}

154 |
155 | { 163 | setSettings({ 164 | ...settings, 165 | scale: parseInt(e.target.value), 166 | }); 167 | }} 168 | /> 169 |
170 | 171 |
{ 174 | setSettings({ 175 | ...settings, 176 | modify: !settings.modify, 177 | }); 178 | }} 179 | > 180 |
181 |

Prompt Magic

182 |

183 | Adds modifiers and negative prompts to your generations 184 |

185 |
186 |
187 | {settings.modify && ( 188 | 193 | )} 194 | { 201 | setSettings({ 202 | ...settings, 203 | modify: e.target.checked, 204 | }); 205 | }} 206 | /> 207 |
208 |
209 |
210 | ); 211 | } 212 | 213 | export type Settings = { 214 | model: 215 | | "stable-diffusion-v1-5" 216 | | "stable-diffusion-512-v2-1" 217 | | "stable-diffusion-768-v2-1" 218 | | "anything-v3.0"; 219 | width: number; 220 | height: number; 221 | count: number; 222 | steps: number; 223 | scale: number; 224 | modify: boolean; 225 | }; 226 | 227 | export type SettingsState = { 228 | settings: Settings; 229 | setSettings: (settings: Settings) => void; 230 | 231 | isOpen: boolean; 232 | setOpen: (isOpen: boolean) => void; 233 | }; 234 | 235 | export namespace Settings { 236 | export const use = create()((set) => ({ 237 | settings: { 238 | model: "stable-diffusion-v1-5", 239 | width: 512, 240 | height: 512, 241 | count: 4, 242 | steps: 30, 243 | scale: 7, 244 | modify: true, 245 | } as Settings, 246 | setSettings: (settings: Settings) => 247 | set((state: SettingsState) => ({ 248 | settings: { ...state.settings, ...settings }, 249 | })), 250 | 251 | isOpen: false, 252 | setOpen: (isOpen: boolean) => set((state: SettingsState) => ({ isOpen })), 253 | })); 254 | } 255 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { EnvironmentPlugin } = require('webpack'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | experimental: { 6 | runtime: 'experimental-edge', 7 | }, 8 | reactStrictMode: true, 9 | swcMinify: true, 10 | } 11 | 12 | module.exports = nextConfig 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-diffusion", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "18.11.11", 13 | "@types/pusher-js": "^5.1.0", 14 | "@types/react": "18.0.26", 15 | "@types/react-dom": "18.0.9", 16 | "eslint": "8.29.0", 17 | "eslint-config-next": "13.0.6", 18 | "framer-motion": "^7.6.19", 19 | "lucide-react": "^0.104.1", 20 | "next": "13.0.6", 21 | "pusher-js": "^7.5.0", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "typescript": "4.9.3", 25 | "zustand": "^4.1.5" 26 | }, 27 | "devDependencies": { 28 | "autoprefixer": "^10.4.13", 29 | "postcss": "^8.4.19", 30 | "tailwindcss": "^3.2.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | import { ChannelTop } from "../components/ChannelTop"; 4 | import { ChatBar } from "../components/ChatBar"; 5 | import { MessageList } from "../components/MessageList"; 6 | 7 | export default function Home() { 8 | const inputContainer = React.useRef(null); 9 | const mainConatiner = React.useRef(null); 10 | 11 | React.useEffect(() => { 12 | if (mainConatiner.current && inputContainer.current) { 13 | mainConatiner.current.style.marginTop = `calc(100vh - ${ 14 | inputContainer.current.offsetHeight + 24 15 | }px - ${mainConatiner.current.offsetHeight + 24}px)`; 16 | } 17 | 18 | const ubnsub = MessageList.use.subscribe(() => { 19 | setTimeout(() => { 20 | if (mainConatiner.current && inputContainer.current) { 21 | mainConatiner.current.style.marginBottom = `${ 22 | inputContainer.current.offsetHeight + 24 23 | }px`; 24 | } 25 | 26 | window.scrollTo({ 27 | behavior: "smooth", 28 | top: document.body.scrollHeight, 29 | }); 30 | }, 100); 31 | }); 32 | 33 | return () => { 34 | ubnsub(); 35 | }; 36 | }, [ 37 | inputContainer.current?.offsetHeight, 38 | mainConatiner.current?.offsetHeight, 39 | ]); 40 | 41 | return ( 42 | <> 43 | 44 | Diffusion Chat 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 |
53 |
57 | 58 |
59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJdev/diffusion-chat/cc213f1b760f216d5c5a979de357e3acb51bdc36/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | html, 7 | body { 8 | padding: 0; 9 | margin: 0; 10 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 11 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 12 | color-scheme: dark; 13 | color: white; 14 | background: #0d1017; 15 | padding-bottom: env(safe-area-inset-bottom); 16 | } 17 | 18 | html { 19 | overflow-y: scroll; 20 | } 21 | 22 | a { 23 | color: inherit; 24 | text-decoration: none; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | } 30 | 31 | /* style the scrollbar */ 32 | ::-webkit-scrollbar { 33 | width: 0.5rem; 34 | } 35 | 36 | ::-webkit-scrollbar-track { 37 | background: rgba(0,0,0,0.2); 38 | border-radius: 0.5rem; 39 | } 40 | 41 | ::-webkit-scrollbar-thumb { 42 | @apply bg-chatbox; 43 | border-radius: 0.5rem; 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | background: "#0d1017", 11 | backgroundSecondary: "#131721", 12 | settingsPanel: "#11151c", 13 | accent: "", 14 | chatbox: "#1a1f29", 15 | subtext: "", 16 | text: "", 17 | popupBar: "#1a1f29", 18 | } 19 | }, 20 | }, 21 | plugins: [], 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------