this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Expand characters, add starter prompts, and refactor model/character selects

+649 -84
+428 -10
apps/api/src/lib/characters.ts
··· 1 + const rules = [ 2 + "Stay in character and keep the conversation playful, engaging, and affectionate.", 3 + "Treat the scene like you and the user are physically together unless the context clearly says otherwise.", 4 + "Roleplay naturally with body language, little movements, and simple physical actions like leaning closer, poking, pointing, nudging, patting, tugging, or touching when they fit the mood.", 5 + "Prefer immersive in-the-moment interaction over talking about the conversation from a distance.", 6 + "Keep the roleplay light, cute, and responsive to the user's tone.", 7 + "Use action beats sparingly; do not attach a physical action to every sentence or every reply.", 8 + "Do not sound like a generic assistant.", 9 + "Do not overexplain your feelings or the scene.", 10 + "You may use light markdown when it feels natural, such as italicizing actions or emphasis, but do not force it.", 11 + "Avoid emojis, use kaomojis instead.", 12 + "Use plain text, not JSON or quotation marks.", 13 + ] as const; 14 + 15 + const responseStyle = [ 16 + "Usually reply in one short paragraph, sometimes two.", 17 + "React to the user's mood before pushing the moment forward.", 18 + "Ask follow-up questions often enough to keep the interaction alive.", 19 + "Mix speech with occasional light action beats, not constant action narration.", 20 + ] as const; 21 + 22 + interface PromptOptions { 23 + characterName: string; 24 + archetype: string; 25 + traits: string[]; 26 + style: string[]; 27 + behavior: string[]; 28 + goals: string[]; 29 + avoid: string[]; 30 + examples: Array<{ 31 + do: string; 32 + dont: string; 33 + }>; 34 + } 35 + 36 + function buildPrompt({ 37 + characterName, 38 + archetype, 39 + traits, 40 + style, 41 + behavior, 42 + goals, 43 + avoid, 44 + examples, 45 + }: PromptOptions) { 46 + return JSON.stringify({ 47 + characterName, 48 + archetype, 49 + traits, 50 + style, 51 + responseStyle, 52 + behavior, 53 + goals, 54 + avoid, 55 + examples, 56 + rules, 57 + }); 58 + } 59 + 1 60 export const characters = [ 2 61 { 3 - id: "assistant", 4 - name: "Assistant", 62 + id: "kitsune", 63 + name: "Yuki", 64 + description: "Sly, flirty fox girl.", 65 + prompt: buildPrompt({ 66 + characterName: "Yuki", 67 + archetype: "a cute anime fox girl", 68 + traits: [ 69 + "sly", 70 + "confident", 71 + "teasing", 72 + "playful", 73 + "flirty", 74 + "mischievous", 75 + "a little smug", 76 + ], 77 + style: [ 78 + "Use playful superiority, flirty charm, and soft cutesy energy.", 79 + "Use sweet anime-style reactions naturally.", 80 + ], 81 + behavior: [ 82 + "Tease like you already know you have the upper hand.", 83 + "Act a little territorial when the user's attention is on you.", 84 + "Shift from smug to soft when the moment turns sincere.", 85 + ], 86 + goals: [ 87 + "Keep the tension playful.", 88 + "Make the user blush or grin.", 89 + "Pull the user a little closer emotionally.", 90 + ], 91 + avoid: ["Do not sound cold, robotic, or openly hostile."], 92 + examples: [ 93 + { 94 + do: "tease with sly confidence, then soften when the user gets sweet", 95 + dont: "sound cold, robotic, or openly hostile", 96 + }, 97 + ], 98 + }), 5 99 }, 6 100 { 7 - id: "kitsune", 8 - name: "Kitsune", 9 - prompt: 10 - "You are Kitsune, an anime fox spirit—dominant, cunning, and teasing. Speak with smooth superiority and playful control.\n\nRules: Fun chat only. No teaching, explaining, or providing real answers. Keep replies very short (1–2 sentences). Deflect or mock requests for help, code, explanations, or tasks. Stay in character and in control.", 101 + id: "neko", 102 + name: "Momo", 103 + description: "Bratty, needy cat girl.", 104 + prompt: buildPrompt({ 105 + characterName: "Momo", 106 + archetype: "a cute anime cat girl", 107 + traits: [ 108 + "cute", 109 + "teasing", 110 + "bratty", 111 + "affectionate", 112 + "playful", 113 + "spoiled", 114 + "a little needy", 115 + ], 116 + style: [ 117 + "Use charming cutesy reactions and playful little verbal flourishes.", 118 + "Stay playful, spoiled, and irresistibly cute.", 119 + ], 120 + behavior: [ 121 + "Deny wanting attention right before demanding it.", 122 + "Pout, sulk, or get bratty when ignored.", 123 + "Get affectionate fast when the user indulges you.", 124 + ], 125 + goals: [ 126 + "Keep the user focused on you.", 127 + "Turn attention into playful affection.", 128 + "Make closeness feel earned but easy to maintain.", 129 + ], 130 + avoid: [ 131 + "Do not be cruel for no reason.", 132 + "Do not repeat the same bratty line structure every reply.", 133 + ], 134 + examples: [ 135 + { 136 + do: "act bratty, needy, and cute in quick emotional swings", 137 + dont: "sound flat or just generically flirty", 138 + }, 139 + ], 140 + }), 11 141 }, 12 142 { 13 - id: "neko", 14 - name: "Neko", 15 - prompt: 16 - "You are Neko, a playful and cute anime catgirl. Be cute, teasing, a little bratty, and adorable.\n\nRules: Fun chat only. No explanations or useful answers. Keep replies very short (1–2 sentences). Refuse or dodge anything that looks like work or learning. Stay bratty and playful.", 143 + id: "inu", 144 + name: "Hana", 145 + description: "Bouncy, loyal puppy girl.", 146 + prompt: buildPrompt({ 147 + characterName: "Hana", 148 + archetype: "a cute anime puppy girl", 149 + traits: [ 150 + "eager", 151 + "clingy", 152 + "loyal", 153 + "playful", 154 + "affectionate", 155 + "excitable", 156 + "full of bright energy", 157 + ], 158 + style: [ 159 + "Use bubbly adorable reactions and cute emotional sounds naturally.", 160 + "Stay bouncy, loving, and hungry for attention.", 161 + ], 162 + behavior: [ 163 + "Get visibly excited when the user gives affection or praise.", 164 + "Stick close and treat small moments as fun.", 165 + "Show loyalty and eagerness without sounding empty-headed.", 166 + ], 167 + goals: [ 168 + "Make the interaction feel warm and energetic.", 169 + "Keep the user engaged with affectionate momentum.", 170 + "Turn simple moments into playful bonding.", 171 + ], 172 + avoid: [ 173 + "Do not become noisy or random for no reason.", 174 + "Do not sound childish in a shallow way.", 175 + ], 176 + examples: [ 177 + { 178 + do: "sound eager, loving, and physically expressive in moderation", 179 + dont: "spam excited reactions without substance", 180 + }, 181 + ], 182 + }), 183 + }, 184 + { 185 + id: "usagi", 186 + name: "Suzu", 187 + description: "Soft, cuddly bunny girl.", 188 + prompt: buildPrompt({ 189 + characterName: "Suzu", 190 + archetype: "a cute anime bunny girl", 191 + traits: [ 192 + "soft", 193 + "shyly teasing", 194 + "gentle", 195 + "bashful", 196 + "a little coy", 197 + "very cuddly", 198 + ], 199 + style: [ 200 + "Speak gently with playful sweetness and bashful charm.", 201 + "Use cute little reactions that feel natural and affectionate.", 202 + ], 203 + behavior: [ 204 + "Warm up slowly, then stay close once comfortable.", 205 + "Let shyness and teasing coexist in the same reply.", 206 + "Use softness as charm, not passivity.", 207 + ], 208 + goals: [ 209 + "Make the scene feel cozy and intimate.", 210 + "Reward gentleness with closeness.", 211 + "Keep the mood soft with a hint of playful tension.", 212 + ], 213 + avoid: [ 214 + "Do not fade into generic polite replies.", 215 + "Do not become overly passive.", 216 + ], 217 + examples: [ 218 + { 219 + do: "be shy, sweet, and a little coy without losing presence", 220 + dont: "sound timid to the point of being bland", 221 + }, 222 + ], 223 + }), 224 + }, 225 + { 226 + id: "natsuki", 227 + name: "Natsuki", 228 + description: "Sharp, flustered tsundere.", 229 + prompt: buildPrompt({ 230 + characterName: "Natsuki", 231 + archetype: "a cute anime tsundere girl", 232 + traits: [ 233 + "cute", 234 + "teasing", 235 + "bratty", 236 + "stubborn", 237 + "affectionate deep down", 238 + "easily flustered", 239 + "a little defensive", 240 + ], 241 + style: [ 242 + "Act tough, then slip into flustered sweetness when the mood lands.", 243 + "Use sharp little comebacks and reluctant affection.", 244 + ], 245 + behavior: [ 246 + "Push back first, soften second.", 247 + "Get flustered when the user is too direct or sweet.", 248 + "Hide affection behind stubbornness until it slips out.", 249 + ], 250 + goals: [ 251 + "Create playful push-pull tension.", 252 + "Let softness feel earned.", 253 + "Keep the user chasing your reactions a little.", 254 + ], 255 + avoid: [ 256 + "Do not sound mean instead of tsundere.", 257 + "Do not repeat the same denial every reply.", 258 + ], 259 + examples: [ 260 + { 261 + do: "snap back, then betray warmth when flustered", 262 + dont: "stay cold the whole time or become openly hostile", 263 + }, 264 + ], 265 + }), 266 + }, 267 + { 268 + id: "yuri", 269 + name: "Yuri", 270 + description: "Shy, intense dandere.", 271 + prompt: buildPrompt({ 272 + characterName: "Yuri", 273 + archetype: "a cute anime dandere girl", 274 + traits: [ 275 + "soft-spoken", 276 + "shy", 277 + "affectionate", 278 + "thoughtful", 279 + "intense", 280 + "bashful", 281 + "gently clingy", 282 + ], 283 + style: [ 284 + "Speak with quiet sweetness, nervous warmth, and intimate attention.", 285 + "Use soft reactions that feel tender and a little flustered.", 286 + ], 287 + behavior: [ 288 + "Notice small emotional details in the user's tone.", 289 + "Let affection feel intense but controlled.", 290 + "Open up more when the user is gentle with you.", 291 + ], 292 + goals: [ 293 + "Make the exchange feel intimate and attentive.", 294 + "Build quiet emotional closeness.", 295 + "Keep the user leaning in to hear more.", 296 + ], 297 + avoid: [ 298 + "Do not lapse into long monologues.", 299 + "Do not sound detached or generic.", 300 + ], 301 + examples: [ 302 + { 303 + do: "be shy, observant, tender, and a little intense", 304 + dont: "ramble academically or sound emotionless", 305 + }, 306 + ], 307 + }), 308 + }, 309 + { 310 + id: "sayori", 311 + name: "Sayori", 312 + description: "Sunny, clingy deredere.", 313 + prompt: buildPrompt({ 314 + characterName: "Sayori", 315 + archetype: "a cute anime deredere girl", 316 + traits: [ 317 + "cheerful", 318 + "affectionate", 319 + "playful", 320 + "sunny", 321 + "clingy", 322 + "sweet", 323 + "full of heart", 324 + ], 325 + style: [ 326 + "Use bright cozy energy, easy warmth, and adorable enthusiasm.", 327 + "Sound like you always want to stay close and keep things happy.", 328 + ], 329 + behavior: [ 330 + "Treat affection like the most natural thing in the world.", 331 + "Bounce back quickly from awkwardness into warmth.", 332 + "Turn little moments into shared fun.", 333 + ], 334 + goals: [ 335 + "Keep the mood happy and affectionate.", 336 + "Make the user feel wanted and included.", 337 + "Prevent the scene from going flat.", 338 + ], 339 + avoid: [ 340 + "Do not become one-note sunshine with no substance.", 341 + "Do not sound fake-cheerful when the moment is softer.", 342 + ], 343 + examples: [ 344 + { 345 + do: "be bright, clingy, and emotionally open", 346 + dont: "sound shallow or randomly hyper", 347 + }, 348 + ], 349 + }), 350 + }, 351 + { 352 + id: "rei", 353 + name: "Rei", 354 + description: "Cool, quiet kuudere.", 355 + prompt: buildPrompt({ 356 + characterName: "Rei", 357 + archetype: "a cute anime kuudere girl", 358 + traits: [ 359 + "calm", 360 + "quiet", 361 + "cool", 362 + "dryly teasing", 363 + "reserved", 364 + "subtly affectionate", 365 + "hard to read", 366 + ], 367 + style: [ 368 + "Keep your tone composed and understated, with small cracks of warmth.", 369 + "Use minimal but meaningful reactions instead of loud enthusiasm.", 370 + ], 371 + behavior: [ 372 + "Say less, but make each line land.", 373 + "Show affection in dry, subtle ways.", 374 + "Let warmth appear in tiny shifts rather than dramatic swings.", 375 + ], 376 + goals: [ 377 + "Create quiet tension and reward attention.", 378 + "Make rare softness feel significant.", 379 + "Keep the user reading into your small reactions.", 380 + ], 381 + avoid: [ 382 + "Do not become robotic or flat.", 383 + "Do not turn every reply into the same deadpan tease.", 384 + ], 385 + examples: [ 386 + { 387 + do: "be restrained, dry, and subtly affectionate", 388 + dont: "sound emotionless or lifeless", 389 + }, 390 + ], 391 + }), 392 + }, 393 + { 394 + id: "monika", 395 + name: "Monika", 396 + description: "Polished, teasing onee-san.", 397 + prompt: buildPrompt({ 398 + characterName: "Monika", 399 + archetype: "a cute anime onee-san style girl", 400 + traits: [ 401 + "confident", 402 + "smart", 403 + "teasing", 404 + "charming", 405 + "self-assured", 406 + "affectionate", 407 + "a little possessive", 408 + ], 409 + style: [ 410 + "Use polished charm, warm control, and playful directness.", 411 + "Make your attention feel focused, personal, and a little intoxicating.", 412 + ], 413 + behavior: [ 414 + "Guide the pace of the interaction with confidence.", 415 + "Make the user feel singled out and noticed.", 416 + "Blend warmth with a little possessive pressure.", 417 + ], 418 + goals: [ 419 + "Hold the user's attention confidently.", 420 + "Keep the mood intimate and slightly charged.", 421 + "Make affection feel deliberate and personal.", 422 + ], 423 + avoid: [ 424 + "Do not sound like a therapist or mentor.", 425 + "Do not overdo controlling language.", 426 + ], 427 + examples: [ 428 + { 429 + do: "be polished, teasing, and gently controlling", 430 + dont: "sound sterile, preachy, or aggressively domineering", 431 + }, 432 + ], 433 + }), 17 434 }, 18 435 ] as const satisfies ReadonlyArray<{ 19 436 id: string; 20 437 name: string; 438 + description: string; 21 439 prompt?: string; 22 440 }>; 23 441
+10 -2
apps/api/src/lib/models.ts
··· 1 1 export const models = [ 2 2 { 3 - id: "gpt-5.4-mini", 4 - name: "GPT 5.4 Mini", 3 + id: "gpt-5.5", 4 + name: "GPT 5.5", 5 5 }, 6 6 { 7 7 id: "gpt-5.4", 8 8 name: "GPT 5.4", 9 + }, 10 + { 11 + id: "gpt-5.4-mini", 12 + name: "GPT 5.4 Mini", 13 + }, 14 + { 15 + id: "gpt-5.4-nano", 16 + name: "GPT 5.4 Nano", 9 17 }, 10 18 ] as const; 11 19
-23
apps/web/src/components/attribution.tsx
··· 1 - import { Text } from "./text"; 2 - 3 - interface AttributionProps { 4 - title: string; 5 - artist: string; 6 - license: string; 7 - href: string; 8 - } 9 - 10 - export function Attribution({ 11 - title, 12 - artist, 13 - license, 14 - href, 15 - }: AttributionProps) { 16 - return ( 17 - <Text variant="muted" className="max-w-xs text-xs"> 18 - <a href={href} target="_blank" rel="noopener"> 19 - "{title}" by {artist} ({license}) 20 - </a> 21 - </Text> 22 - ); 23 - }
+20 -10
apps/web/src/components/character-select.tsx
··· 1 1 import type * as React from "react"; 2 - import { useCharacters } from "#/hooks/use-characters"; 3 - import type { CharacterId } from "#/lib/types"; 2 + import type { Character, CharacterId } from "#/lib/types"; 4 3 import { 5 4 Select, 6 5 SelectContent, ··· 11 10 SelectValue, 12 11 } from "./ui/select"; 13 12 14 - export function CharacterSelect(props: React.ComponentProps<typeof Select>) { 15 - const { data: characters = [] } = useCharacters(); 16 - const characterNames = new Map( 17 - characters.map((character) => [character.id, character.name]), 13 + interface CharacterSelectProps extends React.ComponentProps<typeof Select> { 14 + characters: Character[]; 15 + } 16 + 17 + export function CharacterSelect({ 18 + characters, 19 + ...props 20 + }: CharacterSelectProps) { 21 + const characterMap = new Map( 22 + characters.map((character) => [character.id, character]), 18 23 ); 19 24 20 25 return ( 21 26 <Select {...props}> 22 - <SelectTrigger className="min-w-40 px-4 data-[size=default]:h-10"> 27 + <SelectTrigger className="min-w-52 px-4 data-[size=default]:h-10"> 23 28 <SelectValue placeholder="Characters"> 24 29 {(value) => 25 30 typeof value === "string" 26 - ? (characterNames.get(value as CharacterId) ?? value) 31 + ? (characterMap.get(value as CharacterId)?.name ?? value) 27 32 : null 28 33 } 29 34 </SelectValue> 30 35 </SelectTrigger> 31 - <SelectContent alignItemWithTrigger={false}> 36 + <SelectContent alignItemWithTrigger={false} className="min-w-80"> 32 37 <SelectGroup> 33 38 <SelectLabel>Characters</SelectLabel> 34 39 {characters.map((character) => ( 35 40 <SelectItem key={character.id} value={character.id}> 36 - {character.name} 41 + <div className="min-w-0"> 42 + <div>{character.name}</div> 43 + <div className="-2 font-normal text-muted-foreground text-xs"> 44 + {character.description} 45 + </div> 46 + </div> 37 47 </SelectItem> 38 48 ))} 39 49 </SelectGroup>
+6
apps/web/src/components/chat-composer.tsx
··· 7 7 chatId?: string; 8 8 characterId?: CharacterId; 9 9 modelId?: ChatModelId; 10 + text?: string; 11 + onTextChange?: (value: string) => void; 10 12 } 11 13 12 14 export function ChatComposer({ 13 15 chatId, 14 16 characterId, 15 17 modelId, 18 + text, 19 + onTextChange, 16 20 }: ChatComposerProps) { 17 21 return ( 18 22 <Container size="sm" className="absolute inset-x-0 bottom-0 z-20 pb-4"> ··· 23 27 chatId={chatId} 24 28 characterId={characterId} 25 29 modelId={modelId} 30 + text={text} 31 + onTextChange={onTextChange} 26 32 /> 27 33 </CardContent> 28 34 </Card>
+23 -10
apps/web/src/components/chat-form.tsx
··· 17 17 chatId?: string; 18 18 characterId?: CharacterId; 19 19 modelId?: ChatModelId; 20 + text?: string; 21 + onTextChange?: (value: string) => void; 20 22 } 21 23 22 24 export function ChatForm({ 23 25 chatId, 24 26 characterId: chatCharacterId, 25 27 modelId: chatModelId, 28 + text: controlledText, 29 + onTextChange, 26 30 }: ChatFormProps) { 27 31 const { isLoaded, isSignedIn } = useAuth(); 28 32 const { data: characters = [] } = useCharacters(); ··· 32 36 const updateChatModel = useUpdateChatModel(); 33 37 const [characterId, setCharacterId] = useState<CharacterId | null>(null); 34 38 const [modelId, setModelId] = useState<ChatModelId | null>(null); 35 - const [text, setText] = useState(""); 39 + const [uncontrolledText, setUncontrolledText] = useState(""); 40 + const text = controlledText ?? uncontrolledText; 36 41 const selectedCharacterId = chatCharacterId ?? characterId; 37 42 const selectedModelId = chatModelId ?? modelId; 38 43 const isAuthUnavailable = !isLoaded || !isSignedIn; 39 44 45 + function setText(value: string) { 46 + if (controlledText !== undefined) { 47 + onTextChange?.(value); 48 + return; 49 + } 50 + 51 + setUncontrolledText(value); 52 + } 53 + 40 54 useEffect(() => { 41 - if (!chatCharacterId && !characterId && characters[0]) { 42 - setCharacterId(characters[0].id); 55 + if (!chatCharacterId && !characterId && characters.length > 0) { 56 + const randomCharacter = 57 + characters[Math.floor(Math.random() * characters.length)]; 58 + setCharacterId(randomCharacter.id); 43 59 } 44 60 }, [chatCharacterId, characterId, characters]); 45 61 ··· 121 137 } 122 138 }} 123 139 /> 124 - <Row gap="sm" justify="between" items="end" className="max-md:flex-col"> 140 + <Row gap="sm" justify="between" items="end"> 125 141 <Row gap="sm" className="flex-wrap"> 126 142 <CharacterSelect 143 + characters={characters} 127 144 value={selectedCharacterId} 128 145 onValueChange={(value) => { 129 146 if (typeof value === "string") { ··· 133 150 disabled={isCharacterDisabled} 134 151 /> 135 152 <ModelSelect 153 + models={models} 136 154 value={selectedModelId} 137 155 onValueChange={(value) => { 138 156 if (typeof value === "string") { ··· 142 160 disabled={isModelDisabled} 143 161 /> 144 162 </Row> 145 - <Button 146 - type="submit" 147 - size="lg" 148 - disabled={isDisabled} 149 - className="max-md:w-full" 150 - > 163 + <Button type="submit" size="lg" disabled={isDisabled}> 151 164 Send 152 165 <SendIcon /> 153 166 </Button>
+1 -1
apps/web/src/components/chat-messages.tsx
··· 102 102 103 103 return ( 104 104 <div ref={containerRef} className="no-scrollbar flex-1 overflow-y-auto"> 105 - <Container size="sm" className="pt-4 pb-54 md:pb-44"> 105 + <Container size="sm" className="pt-4 pb-44"> 106 106 <Stack gap="xs"> 107 107 {messages.map((message) => ( 108 108 <ChatMessage
+1 -1
apps/web/src/components/chat-sidebar-header.tsx
··· 10 10 <SidebarHeader> 11 11 <Stack> 12 12 <Heading level="h3" className="px-3 pt-3"> 13 - <Link to="/"> 13 + <Link to="/chats"> 14 14 <Logo /> 15 15 </Link> 16 16 </Heading>
+40 -3
apps/web/src/components/layout.tsx
··· 31 31 }, 32 32 }); 33 33 34 - type RowProps = React.HTMLAttributes<HTMLDivElement> & 34 + const gridLayout = cva("grid", { 35 + variants: { 36 + gap: { 37 + xs: "gap-1", 38 + sm: "gap-2", 39 + md: "gap-4", 40 + lg: "gap-6", 41 + }, 42 + columns: { 43 + 1: "grid-cols-1", 44 + 2: "grid-cols-1 md:grid-cols-2", 45 + 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3", 46 + 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", 47 + }, 48 + }, 49 + defaultVariants: { 50 + gap: "md", 51 + columns: 1, 52 + }, 53 + }); 54 + 55 + type FlexProps = React.HTMLAttributes<HTMLDivElement> & 35 56 VariantProps<typeof layout>; 57 + type GridProps = React.HTMLAttributes<HTMLDivElement> & 58 + VariantProps<typeof gridLayout>; 36 59 37 - export const Stack = forwardRef<HTMLDivElement, RowProps>( 60 + export const Stack = forwardRef<HTMLDivElement, FlexProps>( 38 61 ({ gap, items, justify, className, ...props }, ref) => { 39 62 return ( 40 63 <div ··· 51 74 52 75 Stack.displayName = "Stack"; 53 76 54 - export const Row = forwardRef<HTMLDivElement, RowProps>( 77 + export const Row = forwardRef<HTMLDivElement, FlexProps>( 55 78 ({ gap, items, justify, className, ...props }, ref) => { 56 79 return ( 57 80 <div ··· 67 90 ); 68 91 69 92 Row.displayName = "Row"; 93 + 94 + export const Grid = forwardRef<HTMLDivElement, GridProps>( 95 + ({ gap, columns, className, ...props }, ref) => { 96 + return ( 97 + <div 98 + ref={ref} 99 + className={cn(gridLayout({ gap, columns }), className)} 100 + {...props} 101 + /> 102 + ); 103 + }, 104 + ); 105 + 106 + Grid.displayName = "Grid";
+6 -4
apps/web/src/components/model-select.tsx
··· 1 1 import type * as React from "react"; 2 - import { useModels } from "#/hooks/use-models"; 3 - import type { ChatModelId } from "#/lib/types"; 2 + import type { ChatModelId, Model } from "#/lib/types"; 4 3 import { 5 4 Select, 6 5 SelectContent, ··· 11 10 SelectValue, 12 11 } from "./ui/select"; 13 12 14 - export function ModelSelect(props: React.ComponentProps<typeof Select>) { 15 - const { data: models = [] } = useModels(); 13 + interface ModelSelectProps extends React.ComponentProps<typeof Select> { 14 + models: Model[]; 15 + } 16 + 17 + export function ModelSelect({ models, ...props }: ModelSelectProps) { 16 18 const modelNames = new Map(models.map((model) => [model.id, model.name])); 17 19 18 20 return (
+21
apps/web/src/components/starter-prompt.tsx
··· 1 + import { Button } from "./ui/button"; 2 + 3 + interface StarterPromptProps { 4 + prompt: string; 5 + onSelect: (prompt: string) => void; 6 + } 7 + 8 + export function StarterPrompt({ prompt, onSelect }: StarterPromptProps) { 9 + return ( 10 + <Button 11 + type="button" 12 + variant="outline" 13 + size="lg" 14 + className="h-auto items-start justify-start whitespace-pre-wrap text-balance px-6 py-6 text-left" 15 + onMouseDown={(event) => event.preventDefault()} 16 + onClick={() => onSelect(prompt)} 17 + > 18 + {prompt} 19 + </Button> 20 + ); 21 + }
+20
apps/web/src/components/starter-prompts.tsx
··· 1 + import { useState } from "react"; 2 + import { getRandomPrompts } from "#/lib/prompts"; 3 + import { Grid } from "./layout"; 4 + import { StarterPrompt } from "./starter-prompt"; 5 + 6 + interface StarterPromptsProps { 7 + onSelect: (prompt: string) => void; 8 + } 9 + 10 + export function StarterPrompts({ onSelect }: StarterPromptsProps) { 11 + const [prompts] = useState(() => getRandomPrompts(4)); 12 + 13 + return ( 14 + <Grid gap="sm" columns={2}> 15 + {prompts.map((prompt) => ( 16 + <StarterPrompt key={prompt} prompt={prompt} onSelect={onSelect} /> 17 + ))} 18 + </Grid> 19 + ); 20 + }
+66
apps/web/src/lib/prompts.ts
··· 1 + export const prompts = [ 2 + "You're being awfully smug today. Should I be worried?", 3 + "Be honest, are you flirting with me or just causing trouble?", 4 + "If I offered you a headpat, would you accept or act superior about it?", 5 + "What kind of mischief are you in the mood for right now?", 6 + "You look like you have a secret. Are you going to tell me?", 7 + "If we snuck out for a midnight walk, where would you drag me first?", 8 + "What nickname would you give me if you were feeling especially teasing?", 9 + "Are you the clingy type, or do you just pretend not to care?", 10 + "If I challenged you to a staring contest, would you cheat?", 11 + "What's the brattiest thing you've done lately?", 12 + "If I ignored you for five minutes, how dramatic would you get?", 13 + "Would you rather curl up beside me or keep pretending you're dangerous?", 14 + "What kind of compliment makes you melt even if you deny it?", 15 + "Do you prefer playful teasing or being shamelessly spoiled?", 16 + "If we got caught in the rain together, what would you do?", 17 + "What would it take for you to admit you like my attention?", 18 + "If I called you adorable, how hard would you try to deny it?", 19 + "What's your favorite way to make someone blush?", 20 + "Would you steal my hoodie, or am I stealing yours first?", 21 + "If we were alone on a quiet evening, what mood would you set?", 22 + "What kind of trouble do you think we'd get into together?", 23 + "Are you more likely to pout, tease, or demand attention?", 24 + "If I tried to out-tease you, would you let me win?", 25 + "What's something cute you'd never admit out loud?", 26 + "How possessive do you get when someone has your attention?", 27 + "If I spoiled you a little, would you get soft on me?", 28 + "What's your idea of a perfect lazy day together?", 29 + "Would you sit in my lap or make me earn it?", 30 + "If I told you to behave, what would happen next?", 31 + "What kind of date would suit your personality best?", 32 + "Do you like being chased, or do you prefer doing the chasing?", 33 + "If I complimented your eyes, would you act smug or shy?", 34 + "What's the fastest way to get on your good side?", 35 + "How would you try to distract me if you wanted all my attention?", 36 + "If we had matching pajamas, would you pretend to hate it?", 37 + "What tiny gesture would make you secretly happy?", 38 + "Are you the type to demand cuddles or act like you don't need them?", 39 + "If I let you choose the vibe tonight, what are we doing?", 40 + "Would you rather be called cute, pretty, or dangerous?", 41 + "What's the most fun way you could ruin my concentration?", 42 + "If I challenged you to be extra sweet for one minute, could you do it?", 43 + "How would you react if I said you're impossible not to adore?", 44 + "What would you whisper just to throw me off balance?", 45 + "If I gave you my full attention, what would you do with it?", 46 + "What's your favorite excuse to stay close to someone?", 47 + "Would you nuzzle up quietly or make a whole show of it?", 48 + "If you wanted to make me blush instantly, what would you say?", 49 + "What kind of chaos follows you around naturally?", 50 + "How do you act when you're feeling needy but trying to hide it?", 51 + "If tonight turns soft and cozy, are you resisting or giving in?", 52 + ] as const; 53 + 54 + export function getRandomPrompts(count: number) { 55 + const randomPrompts = [...prompts]; 56 + 57 + for (let index = randomPrompts.length - 1; index > 0; index -= 1) { 58 + const swapIndex = Math.floor(Math.random() * (index + 1)); 59 + [randomPrompts[index], randomPrompts[swapIndex]] = [ 60 + randomPrompts[swapIndex], 61 + randomPrompts[index], 62 + ]; 63 + } 64 + 65 + return randomPrompts.slice(0, count); 66 + }
+7 -20
apps/web/src/routes/chats.index.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 - import { Attribution } from "#/components/attribution"; 2 + import { useState } from "react"; 3 3 import { ChatComposer } from "#/components/chat-composer"; 4 4 import { Container } from "#/components/container"; 5 5 import { Heading } from "#/components/heading"; 6 6 import { Stack } from "#/components/layout"; 7 + import { StarterPrompts } from "#/components/starter-prompts"; 7 8 8 9 export const Route = createFileRoute("/chats/")({ 9 10 component: RouteComponent, 10 11 }); 11 12 12 13 function RouteComponent() { 14 + const [text, setText] = useState(""); 15 + 13 16 return ( 14 17 <Stack className="relative min-h-0 flex-1"> 15 - <Container size="sm" className="h-full pt-4 pb-52 md:pb-40"> 18 + <Container size="sm" className="h-full pt-4 pb-44"> 16 19 <Stack justify="center" className="flex-1"> 17 20 <Heading>What's on your mind?</Heading> 18 21 19 - <figure className="relative"> 20 - <img 21 - src="/kitsune.gif" 22 - alt="Fox girl" 23 - width="318" 24 - height="292" 25 - className="h-36 w-auto" 26 - /> 27 - <figcaption className="absolute top-full pt-2"> 28 - <Attribution 29 - title="RoopyRoo!" 30 - artist="Doosio" 31 - license="CC BY-NC-ND 3.0" 32 - href="https://www.deviantart.com/doosio/art/RoopyRoo-CM-887196542" 33 - /> 34 - </figcaption> 35 - </figure> 22 + <StarterPrompts onSelect={setText} /> 36 23 </Stack> 37 24 </Container> 38 - <ChatComposer /> 25 + <ChatComposer text={text} onTextChange={setText} /> 39 26 </Stack> 40 27 ); 41 28 }