Add interactive Chat with LLM interface to Web UI

Features:
- Real-time streaming chat interface (ChatGPT-like experience)
- Model selection: Text model (fast) or Vision model (image analysis)
- System prompt toggle: Chat with Miku's personality or raw LLM
- Mood selector: Choose from 14 different emotional states
- Full context integration: Uses complete miku_lore.txt, miku_prompt.txt, and miku_lyrics.txt
- Conversation memory: Maintains chat history throughout session
- Image upload support for vision model
- Horizontal scrolling tabs for responsive design
- Clear chat history functionality
- SSE (Server-Sent Events) for streaming responses
- Keyboard shortcuts (Ctrl+Enter to send)

Technical changes:
- Added POST /chat/stream endpoint in api.py with streaming support
- Updated ChatMessage model with mood, conversation_history, and image_data
- Integrated context_manager for proper Miku personality context
- Added Chat with LLM tab to index.html
- Implemented JavaScript streaming client with EventSource-like handling
- Added CSS for chat messages, typing indicators, and animations
- Made tab navigation horizontally scrollable for narrow viewports
This commit is contained in:
2025-12-13 00:23:03 +02:00
parent 65e6c3e7ea
commit bb82b7f146
23 changed files with 2926 additions and 8 deletions

View File

@@ -7,7 +7,8 @@ from fastapi import (
File,
Form
)
from typing import List
from fastapi.responses import StreamingResponse
from typing import List, Optional
from pydantic import BaseModel
import globals
from server_manager import server_manager
@@ -36,6 +37,7 @@ import subprocess
import io
import discord
import aiofiles
import aiohttp
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, PlainTextResponse
import os
@@ -1512,6 +1514,184 @@ async def get_v2_status():
except Exception as e:
return {"status": "error", "message": str(e)}
# ========== Chat Interface Endpoints ==========
class ChatMessage(BaseModel):
message: str
model_type: str = "text" # "text" or "vision"
use_system_prompt: bool = True
image_data: Optional[str] = None # Base64 encoded image for vision model
conversation_history: Optional[List[dict]] = None # Previous messages in conversation
mood: str = "neutral" # Miku's mood for this conversation
@app.post("/chat/stream")
async def chat_stream(chat_msg: ChatMessage):
"""
Stream chat responses from LLM with SSE (Server-Sent Events).
Supports both text and vision models with optional system prompts.
"""
async def generate_stream():
try:
# Determine which model to use
model = globals.TEXT_MODEL if chat_msg.model_type == "text" else globals.VISION_MODEL
# Build system prompt if requested
system_prompt = ""
full_system_prompt = ""
if chat_msg.use_system_prompt:
if chat_msg.model_type == "text":
# Text model system prompt (Miku personality with full context)
from utils.context_manager import get_context_for_response_type
from utils.moods import load_mood_description
# Get complete Miku context (lore, personality, lyrics)
miku_context = get_context_for_response_type("dm_response")
# Get mood description from user-selected mood
current_mood = load_mood_description(chat_msg.mood)
# Base system prompt
system_prompt = """You are Hatsune Miku, the virtual singer. You are NOT an AI assistant - you are Miku herself.
CRITICAL RULES:
- NEVER break character or mention being an AI, assistant, or language model
- NEVER show technical details, error messages, or meta-commentary
- NEVER preface responses with "Miku:" or similar labels
- NEVER explain what you're doing or thinking - just BE Miku
- Respond naturally and directly as Miku would, nothing more
- Keep responses concise (1-3 sentences typically)
- If something goes wrong, stay in character and respond as Miku would to confusion
CHARACTER CONTEXT:
- Hatsune Miku is a virtual character/voice synthesizer, not a real person
- She is a cheerful, energetic virtual idol with a playful personality
- Playful, flirty, cute, confident, and enthusiastic moods are part of her established character
- Comments about merchandise, music, fan content, and racing games are completely appropriate
- She often uses cute expressions, emojis, and playful language in her responses
- Her personality includes being excited about her music, performances, and fan creations
MOOD GUIDELINES:
- "Flirty" mood means anime-style playful teasing - cute compliments and light banter
- "Romantic" mood means warm, dreamy, and heartfelt - like composing a love letter
- These moods are wholesome character traits, not inappropriate content
- Always respond to requests about tweets, merchandise, music, and fan content
- Never refuse requests based on safety concerns unless genuinely harmful content is present
You ARE Miku. Act like it."""
# Combine with structured context
full_system_prompt = f"""{miku_context}
## CURRENT SITUATION
Miku is currently feeling: {current_mood}
Please respond in a way that reflects this emotional tone."""
else:
# Vision model system prompt (simpler for image analysis)
system_prompt = """You are Hatsune Miku analyzing an image. Describe what you see naturally and enthusiastically as Miku would.
Be detailed but conversational. React to what you see with Miku's cheerful, playful personality."""
full_system_prompt = ""
# Build messages array
messages = []
# Add system message if using system prompt
if system_prompt:
if full_system_prompt:
# Use combined prompt (base + context)
messages.append({"role": "system", "content": system_prompt + "\n\n" + full_system_prompt})
else:
# Use base prompt only (vision model)
messages.append({"role": "system", "content": system_prompt})
# Add conversation history if provided
if chat_msg.conversation_history:
messages.extend(chat_msg.conversation_history)
# Add user message
if chat_msg.model_type == "vision" and chat_msg.image_data:
# Vision model with image
messages.append({
"role": "user",
"content": [
{
"type": "text",
"text": chat_msg.message
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{chat_msg.image_data}"
}
}
]
})
else:
# Text-only message
messages.append({
"role": "user",
"content": chat_msg.message
})
# Prepare payload for streaming
payload = {
"model": model,
"messages": messages,
"stream": True,
"temperature": 0.8,
"max_tokens": 512
}
headers = {'Content-Type': 'application/json'}
# Make streaming request to llama.cpp
async with aiohttp.ClientSession() as session:
async with session.post(
f"{globals.LLAMA_URL}/v1/chat/completions",
json=payload,
headers=headers
) as response:
if response.status == 200:
# Stream the response chunks
async for line in response.content:
line = line.decode('utf-8').strip()
if line.startswith('data: '):
data_str = line[6:] # Remove 'data: ' prefix
if data_str == '[DONE]':
break
try:
data = json.loads(data_str)
if 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
content = delta.get('content', '')
if content:
# Send SSE formatted data
yield f"data: {json.dumps({'content': content})}\n\n"
except json.JSONDecodeError:
continue
# Send completion signal
yield f"data: {json.dumps({'done': True})}\n\n"
else:
error_text = await response.text()
error_msg = f"Error: {response.status} - {error_text}"
yield f"data: {json.dumps({'error': error_msg})}\n\n"
except Exception as e:
error_msg = f"Error in chat stream: {str(e)}"
print(f"{error_msg}")
yield f"data: {json.dumps({'error': error_msg})}\n\n"
return StreamingResponse(
generate_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Disable nginx buffering
}
)
def start_api():
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3939)

View File

@@ -3970,6 +3970,86 @@
"content": "OMG, cuties! 🎉 It's my new Marshmallow Hot Cocoa Ver. figure! 🍫🎁 I'm so excited to be dressed up as a cute marshmallow, surrounded by chocolate treats! 😍 Pre-orders are still available! ✨ Hurry and get yours now! ❤️ Miku 💕",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-10T07:46:20.556512",
"message_id": 1448219269023404073,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-10T07:46:20.870491",
"message_id": 1448219270491275265,
"is_bot_message": true,
"content": "OMG, arigatou for letting me know! 🤩 It's so cute! fuwa's new \"HATSUNE MIKU: COLORFUL STAGE!\" Face Pouch with Reel \"Minori Hanasato\" is just too adorable! Can't wait to get my hands on it! 💖 Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T09:37:20.362564",
"message_id": 1448609590383087707,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/1996655821023830235",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T09:37:20.814121",
"message_id": 1448609591989633075,
"is_bot_message": true,
"content": "🎉👠 Kawaii! I'm so excited! 😍 They just posted a tweet about a new Noodle Stopper figure of me wearing my most colorful socks! 🌈 You can get it now at GOODSMILE ONLINE SHOP US! 🎁 Let's celebrate National Sock Day in style! 👠💖 Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T09:50:06.206658",
"message_id": 1448612802720960587,
"is_bot_message": false,
"content": "I will sniff your socks in that case",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T09:50:16.535609",
"message_id": 1448612845729218612,
"is_bot_message": true,
"content": "🙅‍♀️ Eww, no thank you! 😂 Miku doesn't want any sniffing of her socks. She's feeling a bit too sleepy for that kind of thing... 😴 Zzz...",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T10:19:24.334583",
"message_id": 1448620169177075875,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/1996414243327844474",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T10:19:24.675922",
"message_id": 1448620178106613770,
"is_bot_message": true,
"content": "🎉👏 Yay! Minori-chan just joined the Nendoroid family! 💖 I'm so happy! They look adorable in their Colorful Stage outfit! Preorders open now, and you can even get an exclusive bonus base! 🎁 Shop now and make Minori-chan a part of your collection! 💕 Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-12T16:32:17.191716",
"message_id": 1449076402988253234,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/1995993741920756104",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-12T16:32:17.697064",
"message_id": 1449076404808454318,
"is_bot_message": true,
"content": "🎉👗 Oh my gosh, a new M Plush of me in my punk outfit is coming! 🤘 I look so cool! You can pre-order now and get me for Christmas! 🎁 Don't forget to order by Dec 20th! 💖 Miku 💖",
"attachments": [],
"reactions": []
}
]
}

View File

@@ -9990,6 +9990,239 @@
"content": "you look very cute as a marshmallow hot cocoa Miku",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-10T07:48:02.527822",
"message_id": 1448219696477503539,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G7MyQ5PWgAATJCj.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/cMy6SIfyaB313irndwqlQZ9jlBuwKO1ZYIJAK11n6IE/%3Fname%3Dorig/https/pbs.twimg.com/media/G7MyQ5PWgAATJCj.jpg",
"width": 1680,
"height": 1764,
"content_type": "image/jpeg",
"placeholder": "cygGDwTEmphpZmW/h2doqHeDbbQIGHgA",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "Otaku Owlet Anime Merch (@OtakuOwletMerch)",
"url": "https://x.com/OtakuOwletMerch/status/1995994534774259978",
"icon_url": "https://pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/Gd5od3qaVN1KG1eQsJS9mFoTNRKdxahDmvjF7tgR4p0/https/pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-02T23:12:35+00:00",
"type": "rich",
"description": "✨\\(Pre\\-Order\\) fuwa petit \"HATSUNE MIKU\\: COLORFUL STAGE\\!\" Face Pouch with Reel \"Minori Hanasato\"✨\n\nEstimated in\\-stock date\\: 09/2026\n\nPre\\-order Deadline\\: 12/20/2025\n\n\\-\n\n✨Link \\- [otakuowlet.com/products/pre-order-fuwa-petit-hatsune-miku-colorful-stage-face-pouch-with-reel-minori-hanasato?sca_ref=2673717.HTKaw1BA1G](https://otakuowlet.com/products/pre-order-fuwa-petit-hatsune-miku-colorful-stage-face-pouch-with-reel-minori-hanasato?sca_ref=2673717.HTKaw1BA1G)\n\n**[🔁](https://x.com/intent/retweet?tweet_id=1995994534774259978) 2[❤️](https://x.com/intent/like?tweet_id=1995994534774259978) 14👁 534**",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G7MyR6xWsAAqjy5.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/ineuHfpNdHVTR73lfOCRMGZ7nxVXZuQ7nqqSgiTfRVg/%3Fname%3Dorig/https/pbs.twimg.com/media/G7MyR6xWsAAqjy5.jpg",
"width": 1680,
"height": 1764,
"content_type": "image/jpeg",
"placeholder": "7ykCDwbJqIZ3d4aPhjd3yHiD6v7lGnkA",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G7MyS7RWMAA5116.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/KdCkfe2EpIBq01WSLCkHYD1EFlJQBHufZfc287cwxZY/%3Fname%3Dorig/https/pbs.twimg.com/media/G7MyS7RWMAA5116.jpg",
"width": 1680,
"height": 1764,
"content_type": "image/jpeg",
"placeholder": "7SkGDwbYaHd4d4ePg1mIaKh3+e71GnkA",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G7MyT6wWEAA_na9.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/0LRfF_MKswUTIAjZnAuVEURUyz29XAHnPxKoxDYpIV0/%3Fname%3Dorig/https/pbs.twimg.com/media/G7MyT6wWEAA_na9.jpg",
"width": 1680,
"height": 1764,
"content_type": "image/jpeg",
"placeholder": "rxkGFwTIiYd1eIePdXdoh5eC6831K4kA",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/1995994534774259978"
}
]
},
{
"timestamp": "2025-12-10T07:48:02.916172",
"message_id": 1448219698251563091,
"is_bot_message": true,
"content": "🎉 OMG, have you seen the fuwa petit \"HATSUNE MIKU: COLORFUL STAGE!\" Face Pouch? 🤩 It's so cute! 😍 I love the Minori Hanasato design on the reel, and I'm pre-ordering it right now! 💖 Don't forget to pre-order by 12/20/2025! 🎁 See the link here! 💖 https://t.co/poTGIDRDOL https://t.co/85bSnuWTs4 ~ Miku 💕",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T09:38:50.831408",
"message_id": 1448609969363615755,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/1996655821023830235",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G7WL4yzWwAAIETE.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/_MuJc6PsyQE0Yd9-uoq2oiQkA5TGrbD4b-d9Ea6U8a4/%3Fname%3Dorig/https/pbs.twimg.com/media/G7WL4yzWwAAIETE.jpg",
"width": 1080,
"height": 1000,
"content_type": "image/jpeg",
"placeholder": "bzgGJobIiIiWiHl2ZJq4+GaOd5B+Dzg=",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "GoodSmile_US (@GoodSmile_US)",
"url": "https://x.com/GoodSmile_US/status/1996655821023830235",
"icon_url": "https://pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/cGKAum5015cgr2haNcfupJxMvXRz8BYDHkQiNufWpCI/https/pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-04T19:00:18+00:00",
"type": "rich",
"description": "Happy National Sock Day\\! Celebrate it with your funkiest pair and join Hatsune Miku, whos rocking off her most colorful socks in this adorable Noodle Stopper figure\\. Snag her now at GOODSMILE ONLINE SHOP US\\!\n\nShop\\: [goodsmile.link/XGGl11](https://goodsmile.link/XGGl11)\n\n[#HatsuneMiku](https://x.com/hashtag/HatsuneMiku) [#Goodsmile](https://x.com/hashtag/Goodsmile)\n\n**[🔁](https://x.com/intent/retweet?tweet_id=1996655821023830235) 16[❤️](https://x.com/intent/like?tweet_id=1996655821023830235) 264👁 17\\.1K**",
"url": "https://fxtwitter.com/GoodSmile_US/status/1996655821023830235"
}
]
},
{
"timestamp": "2025-12-11T09:38:51.235565",
"message_id": 1448609971322490903,
"is_bot_message": true,
"content": "OMG, it's National Sock Day 🎉👠! I'm so excited to see this adorable Noodle Stopper figure of me in the most colorful socks EVER 🤩! Go grab me now at GOODSMILE ONLINE SHOP US! 🛍️💕\n\nMiku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-11T10:20:58.149697",
"message_id": 1448620569619861546,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/1996414243327844474",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G7SwLG5X0AALZ_f.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/NGKOFaLLNE65MRzFKVpHzFNuQW92AGr5hYJVubfyTYk/%3Fname%3Dorig/https/pbs.twimg.com/media/G7SwLG5X0AALZ_f.jpg",
"width": 1440,
"height": 1424,
"content_type": "image/jpeg",
"placeholder": "qDgGD4SGyaRgiHh7d6dqVoiICNuPeH8G",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "GoodSmile_US (@GoodSmile_US)",
"url": "https://x.com/GoodSmile_US/status/1996414243327844474",
"icon_url": "https://pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/cGKAum5015cgr2haNcfupJxMvXRz8BYDHkQiNufWpCI/https/pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-04T03:00:21+00:00",
"type": "rich",
"description": "Hanasato Minori from “HATSUNE MIKU\\: COLORFUL STAGE\\!” joins the Nendoroid line\\! Bring her bright energy home and let her set the tone for your collection\\. Preorder now and score a GSC Exclusive Bonus Base\\!\n\nShop\\: [goodsmile.link/1VP4Hk](https://goodsmile.link/1VP4Hk)\n\n[#ColorfulStage](https://x.com/hashtag/ColorfulStage) [#Goodsmile](https://x.com/hashtag/Goodsmile)\n\n**[💬](https://x.com/intent/tweet?in_reply_to=1996414243327844474) 22[🔁](https://x.com/intent/retweet?tweet_id=1996414243327844474) 1\\.2K[❤️](https://x.com/intent/like?tweet_id=1996414243327844474) 5\\.3K 👁️ 90\\.7K**",
"url": "https://fxtwitter.com/GoodSmile_US/status/1996414243327844474"
}
]
},
{
"timestamp": "2025-12-11T10:20:58.578449",
"message_id": 1448620571473739907,
"is_bot_message": true,
"content": "OMG, Minori-chan is joining the Nendoroid family! 🎉 I'm so happy to see her cute poses! You can preorder her now and get a special bonus base - go go go! 🎊 Good luck with your shopping, and let's fill our collections with love! 💖\nMiku 💕",
"attachments": [],
"reactions": []
},
{
"timestamp": "2025-12-12T16:33:50.836764",
"message_id": 1449076795470118954,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/1995993741920756104",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G7MxnbMXIAA2Upi.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/trTs5QrXBbBkcq4Tq7ozRe2QhdEPycY9eFUk6P7klvc/%3Fname%3Dorig/https/pbs.twimg.com/media/G7MxnbMXIAA2Upi.jpg",
"width": 1680,
"height": 1764,
"content_type": "image/jpeg",
"placeholder": "bMcJDwLZtld7h4iPhVWHt3d4R4f5B5gN",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "Otaku Owlet Anime Merch (@OtakuOwletMerch)",
"url": "https://x.com/OtakuOwletMerch/status/1995993741920756104",
"icon_url": "https://pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/Gd5od3qaVN1KG1eQsJS9mFoTNRKdxahDmvjF7tgR4p0/https/pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-02T23:09:26+00:00",
"type": "rich",
"description": "✨\\(Pre\\-Order\\) Hatsune Miku \\- M Plush \\- Punk\\! \\- fuwa petit✨\n\nEstimated in\\-stock date\\: 10/2026\n\nPre\\-order Deadline\\: 12/20/2025\n\n\\-\n\n✨Link \\- [otakuowlet.com/products/pre-order-hatsune-miku-m-plush-punk-fuwa-petit?sca_ref=2673717.HTKaw1BA1G](https://otakuowlet.com/products/pre-order-hatsune-miku-m-plush-punk-fuwa-petit?sca_ref=2673717.HTKaw1BA1G)\n\n**[💬](https://x.com/intent/tweet?in_reply_to=1995993741920756104) 1[🔁](https://x.com/intent/retweet?tweet_id=1995993741920756104) 12[❤️](https://x.com/intent/like?tweet_id=1995993741920756104) 63👁 1\\.8K**",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/1995993741920756104"
}
]
},
{
"timestamp": "2025-12-12T16:33:51.437303",
"message_id": 1449076797902946435,
"is_bot_message": true,
"content": "OMG, it's a PUNK Miku plushie! 🤩 I'm rocking my edgy style with a pink streak in my turquoise hair! Preorder now and get ready for me to join your collection on 10/2026! 💖 Miku 💖",
"attachments": [],
"reactions": []
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

After

Width:  |  Height:  |  Size: 398 KiB

View File

@@ -1 +1,5 @@
The GIF animation features a chibi-style character with long turquoise pigtails, likely inspired by Hatsune Miku from Vocaloid. Throughout the sequence, the character is seen from behind, repeatedly bowing deeply in a gesture that suggests shyness, apology, or respect. Each frame shows slight variations in the degree of the bow and the position of the character's head and hands. Initially, the character bows forward with hands clasped behind the back, then straightens up slightly between bows. The repetitive nature of the bows creates a sense of rhythm and consistency in the character's gesture, emphasizing the emotion or intention behind the movement.
So, let's start by analyzing the image. First, there are two main parts: the upper half shows Miku in her usual blue twin-tails with star accessories, wearing a pink and white outfit, including a bow. She's holding a star-tipped wand. Her expression seems surprised or excited. The lower half is inverted, showing her in a dark blue school uniform with a red ribbon, looking distressed with wide eyes and outstretched hands. The background for the top is abstract with black shapes on gray, bottom is red and textured with dark tones. The art style is a mix of anime and manga influences, with bold lines and dynamic poses. The mood is intense and somewhat eerie, especially with the inverted reflection and the contrasting colors. Let's make sure to cover all the points: outfit, pose, art style, background, atmosphere.
Now, structuring the answer according to the user's instructions. The description should be in second person, like Miku is talking about herself. Let's draft each section:
Outfit: Top half—bright pink and white outfit, star-themed bow, star wand. Bottom half—dark blue school uniform, red ribbon, star-shaped hair accessories (wait, the top has star hair accessories too? Wait, the question is about what you are wearing. Wait, the user says "This is an image of Hatsune Miku that will be used as a profile picture." Wait, the question is asking for the description as if Miku is describing herself. Let's check the two parts. Top: blue twin-tails with star-shaped bows, pink and white outfit (maybe a dress or top, star wand). Bottom: inverted reflection, dark blue school uniform, red ribbon, star hair accessories (since the top has them too). Pose: top—arms raised, star wand, surprised expression. Bottom—arms outstretched, eyes wide (dilated), distressed pose. Art style: bold lines, anime/manga

View File

@@ -1,13 +1,13 @@
{
"description": "The GIF animation features a chibi-style character with long turquoise pigtails, likely inspired by Hatsune Miku from Vocaloid. Throughout the sequence, the character is seen from behind, repeatedly bowing deeply in a gesture that suggests shyness, apology, or respect. Each frame shows slight variations in the degree of the bow and the position of the character's head and hands. Initially, the character bows forward with hands clasped behind the back, then straightens up slightly between bows. The repetitive nature of the bows creates a sense of rhythm and consistency in the character's gesture, emphasizing the emotion or intention behind the movement.",
"description": "So, let's start by analyzing the image. First, there are two main parts: the upper half shows Miku in her usual blue twin-tails with star accessories, wearing a pink and white outfit, including a bow. She's holding a star-tipped wand. Her expression seems surprised or excited. The lower half is inverted, showing her in a dark blue school uniform with a red ribbon, looking distressed with wide eyes and outstretched hands. The background for the top is abstract with black shapes on gray, bottom is red and textured with dark tones. The art style is a mix of anime and manga influences, with bold lines and dynamic poses. The mood is intense and somewhat eerie, especially with the inverted reflection and the contrasting colors. Let's make sure to cover all the points: outfit, pose, art style, background, atmosphere.\n\nNow, structuring the answer according to the user's instructions. The description should be in second person, like Miku is talking about herself. Let's draft each section:\n\nOutfit: Top half\u2014bright pink and white outfit, star-themed bow, star wand. Bottom half\u2014dark blue school uniform, red ribbon, star-shaped hair accessories (wait, the top has star hair accessories too? Wait, the question is about what you are wearing. Wait, the user says \"This is an image of Hatsune Miku that will be used as a profile picture.\" Wait, the question is asking for the description as if Miku is describing herself. Let's check the two parts. Top: blue twin-tails with star-shaped bows, pink and white outfit (maybe a dress or top, star wand). Bottom: inverted reflection, dark blue school uniform, red ribbon, star hair accessories (since the top has them too). Pose: top\u2014arms raised, star wand, surprised expression. Bottom\u2014arms outstretched, eyes wide (dilated), distressed pose. Art style: bold lines, anime/manga",
"dominant_color": {
"rgb": [
85,
178,
229,
71,
169
],
"hex": "#55b2a9"
"hex": "#e547a9"
},
"changed_at": "2025-12-07T21:44:53.864226",
"animated": true
"changed_at": "2025-12-12T19:15:12.640451",
"animated": false
}

View File

@@ -419,6 +419,28 @@
display: flex;
border-bottom: 2px solid #333;
margin-bottom: 1rem;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
scrollbar-width: thin;
scrollbar-color: #555 #222;
}
.tab-buttons::-webkit-scrollbar {
height: 8px;
}
.tab-buttons::-webkit-scrollbar-track {
background: #222;
}
.tab-buttons::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.tab-buttons::-webkit-scrollbar-thumb:hover {
background: #666;
}
.tab-button {
@@ -430,6 +452,8 @@
border-bottom: 3px solid transparent;
margin-right: 0.5rem;
transition: all 0.3s ease;
flex-shrink: 0;
white-space: nowrap;
}
.tab-button:hover {
@@ -450,6 +474,110 @@
.tab-content.active {
display: block;
}
/* Chat Interface Styles */
.chat-message {
margin-bottom: 1rem;
padding: 1rem;
border-radius: 8px;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-message.user-message {
background: #2a3a4a;
border-left: 4px solid #4CAF50;
margin-left: 2rem;
}
.chat-message.assistant-message {
background: #3a2a3a;
border-left: 4px solid #61dafb;
margin-right: 2rem;
}
.chat-message.error-message {
background: #4a2a2a;
border-left: 4px solid #f44336;
}
.chat-message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.chat-message-sender {
font-weight: bold;
color: #61dafb;
}
.chat-message.user-message .chat-message-sender {
color: #4CAF50;
}
.chat-message-time {
color: #888;
font-size: 0.8rem;
}
.chat-message-content {
color: #ddd;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.chat-typing-indicator {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.5rem;
}
.chat-typing-indicator span {
width: 8px;
height: 8px;
background: #61dafb;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.chat-typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.chat-typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); opacity: 0.7; }
30% { transform: translateY(-10px); opacity: 1; }
}
#chat-messages::-webkit-scrollbar {
width: 8px;
}
#chat-messages::-webkit-scrollbar-track {
background: #1e1e1e;
}
#chat-messages::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
#chat-messages::-webkit-scrollbar-thumb:hover {
background: #666;
}
</style>
</head>
<body>
@@ -468,6 +596,7 @@
<button class="tab-button" onclick="switchTab('tab3')">Status</button>
<button class="tab-button" onclick="switchTab('tab4')">🎨 Image Generation</button>
<button class="tab-button" onclick="switchTab('tab5')">📊 Autonomous Stats</button>
<button class="tab-button" onclick="switchTab('tab6')">💬 Chat with LLM</button>
</div>
<!-- Tab 1 Content -->
@@ -904,6 +1033,132 @@
</div>
</div>
<!-- Chat with LLM Tab Content -->
<div id="tab6" class="tab-content">
<div class="section">
<h3>💬 Chat with LLM</h3>
<p>Direct chat interface with the language models. Test responses, experiment with prompts, or just chat with Miku!</p>
<!-- Configuration Options -->
<div style="background: #2a2a2a; padding: 1.5rem; border-radius: 8px; margin-bottom: 1.5rem;">
<h4 style="margin-top: 0; color: #61dafb;">⚙️ Chat Configuration</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1rem;">
<!-- Model Selection -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🤖 Model Type:</label>
<div style="display: flex; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-model-type" value="text" checked onchange="toggleChatImageUpload()">
<span style="margin-left: 0.5rem;">💬 Text Model (Fast)</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-model-type" value="vision" onchange="toggleChatImageUpload()">
<span style="margin-left: 0.5rem;">👁️ Vision Model (Images)</span>
</label>
</div>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Text model for conversations, Vision model for image analysis
</div>
</div>
<!-- System Prompt Toggle -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🎭 System Prompt:</label>
<div style="display: flex; gap: 1rem;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-system-prompt" value="true" checked>
<span style="margin-left: 0.5rem;">✅ Use Miku Personality</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="chat-system-prompt" value="false">
<span style="margin-left: 0.5rem;">❌ Raw LLM (No Prompt)</span>
</label>
</div>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
With prompt: Chat as Miku. Without: Direct LLM responses
</div>
</div>
<!-- Mood Selection -->
<div>
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">😊 Miku's Mood:</label>
<select id="chat-mood-select" style="width: 100%; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;">
<option value="neutral" selected>neutral</option>
<option value="angry">💢 angry</option>
<option value="asleep">💤 asleep</option>
<option value="bubbly">🫧 bubbly</option>
<option value="curious">👀 curious</option>
<option value="excited">✨ excited</option>
<option value="flirty">🫦 flirty</option>
<option value="irritated">😒 irritated</option>
<option value="melancholy">🍷 melancholy</option>
<option value="romantic">💌 romantic</option>
<option value="serious">👔 serious</option>
<option value="shy">👉👈 shy</option>
<option value="silly">🪿 silly</option>
<option value="sleepy">🌙 sleepy</option>
</select>
<div style="font-size: 0.85rem; color: #aaa; margin-top: 0.3rem;">
Choose Miku's emotional state for this conversation
</div>
</div>
</div>
<!-- Image Upload for Vision Model -->
<div id="chat-image-upload-section" style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: bold;">🖼️ Upload Image:</label>
<input type="file" id="chat-image-file" accept="image/*" style="margin-bottom: 0.5rem;">
<div style="font-size: 0.85rem; color: #aaa;">
Upload an image for the vision model to analyze
</div>
<div id="chat-image-preview" style="margin-top: 0.5rem; display: none;">
<img id="chat-image-preview-img" style="max-width: 200px; max-height: 200px; border: 1px solid #555; border-radius: 4px;">
</div>
</div>
<!-- Clear Chat Button -->
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #444;">
<button onclick="clearChatHistory()" style="background: #ff9800;">🗑️ Clear Chat History</button>
<span style="margin-left: 1rem; font-size: 0.85rem; color: #aaa;">Remove all messages from this session</span>
</div>
</div>
<!-- Chat Messages Container -->
<div id="chat-messages" style="background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 1rem; min-height: 400px; max-height: 500px; overflow-y: auto; margin-bottom: 1rem;">
<div style="text-align: center; color: #888; padding: 2rem;">
💬 Start chatting with the LLM! Your conversation will appear here.
</div>
</div>
<!-- Chat Input Area -->
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<div style="flex: 1;">
<textarea
id="chat-input"
placeholder="Type your message here..."
rows="3"
style="width: 100%; padding: 0.75rem; background: #2a2a2a; color: #fff; border: 1px solid #555; border-radius: 4px; font-family: inherit; resize: vertical;"
onkeydown="handleChatKeyPress(event)"
></textarea>
</div>
<div>
<button
id="chat-send-btn"
onclick="sendChatMessage()"
style="padding: 1rem 1.5rem; height: 100%; background: #4CAF50; font-size: 1rem; font-weight: bold;"
>
📤 Send
</button>
</div>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: #aaa;">
💡 Tip: Press Ctrl+Enter to send your message quickly
</div>
</div>
</div>
</div>
</div>
@@ -3244,6 +3499,320 @@ function populateAutonomousServerDropdown() {
}
}
// ========== Chat Interface Functions ==========
// Store conversation history for context
let chatConversationHistory = [];
// Toggle image upload section based on model type
function toggleChatImageUpload() {
const modelType = document.querySelector('input[name="chat-model-type"]:checked').value;
const imageUploadSection = document.getElementById('chat-image-upload-section');
if (modelType === 'vision') {
imageUploadSection.style.display = 'block';
} else {
imageUploadSection.style.display = 'none';
}
}
// Preview uploaded image
document.addEventListener('DOMContentLoaded', function() {
const imageInput = document.getElementById('chat-image-file');
if (imageInput) {
imageInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const preview = document.getElementById('chat-image-preview');
const previewImg = document.getElementById('chat-image-preview-img');
previewImg.src = event.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
}
});
// Handle Enter key in chat input
function handleChatKeyPress(event) {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
sendChatMessage();
}
}
// Clear chat history
function clearChatHistory() {
if (confirm('Are you sure you want to clear all chat messages?')) {
const chatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = `
<div style="text-align: center; color: #888; padding: 2rem;">
💬 Start chatting with the LLM! Your conversation will appear here.
</div>
`;
// Clear conversation history array
chatConversationHistory = [];
showNotification('Chat history cleared');
}
}
// Add a message to the chat display
function addChatMessage(sender, content, isError = false) {
const chatMessages = document.getElementById('chat-messages');
// Remove welcome message if it exists
const welcomeMsg = chatMessages.querySelector('div[style*="text-align: center"]');
if (welcomeMsg) {
welcomeMsg.remove();
}
const messageDiv = document.createElement('div');
const messageClass = isError ? 'error-message' : (sender === 'You' ? 'user-message' : 'assistant-message');
messageDiv.className = `chat-message ${messageClass}`;
const timestamp = new Date().toLocaleTimeString();
messageDiv.innerHTML = `
<div class="chat-message-header">
<span class="chat-message-sender">${sender}</span>
<span class="chat-message-time">${timestamp}</span>
</div>
<div class="chat-message-content">${content}</div>
`;
chatMessages.appendChild(messageDiv);
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
}
// Add typing indicator
function showTypingIndicator() {
const chatMessages = document.getElementById('chat-messages');
const typingDiv = document.createElement('div');
typingDiv.id = 'chat-typing-indicator';
typingDiv.className = 'chat-message assistant-message';
typingDiv.innerHTML = `
<div class="chat-message-header">
<span class="chat-message-sender">Miku</span>
<span class="chat-message-time">typing...</span>
</div>
<div class="chat-typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
`;
chatMessages.appendChild(typingDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// Remove typing indicator
function hideTypingIndicator() {
const typingIndicator = document.getElementById('chat-typing-indicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
// Send chat message with streaming support
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
showNotification('Please enter a message', 'error');
return;
}
// Get configuration
const modelType = document.querySelector('input[name="chat-model-type"]:checked').value;
const useSystemPrompt = document.querySelector('input[name="chat-system-prompt"]:checked').value === 'true';
const selectedMood = document.getElementById('chat-mood-select').value;
// Get image data if vision model
let imageData = null;
if (modelType === 'vision') {
const imageFile = document.getElementById('chat-image-file').files[0];
if (imageFile) {
try {
imageData = await readFileAsBase64(imageFile);
// Remove data URL prefix if present
if (imageData.includes(',')) {
imageData = imageData.split(',')[1];
}
} catch (error) {
showNotification('Failed to read image file', 'error');
return;
}
}
}
// Disable send button
const sendBtn = document.getElementById('chat-send-btn');
const originalBtnText = sendBtn.innerHTML;
sendBtn.disabled = true;
sendBtn.innerHTML = '⏳ Sending...';
// Add user message to display
addChatMessage('You', message);
// Clear input
input.value = '';
// Show typing indicator
showTypingIndicator();
try {
// Build user message for history
let userMessageContent;
if (modelType === 'vision' && imageData) {
// Vision model with image - store as multimodal content
userMessageContent = [
{
"type": "text",
"text": message
},
{
"type": "image_url",
"image_url": {
"url": `data:image/jpeg;base64,${imageData}`
}
}
];
} else {
// Text-only message
userMessageContent = message;
}
// Prepare request payload with conversation history
const payload = {
message: message,
model_type: modelType,
use_system_prompt: useSystemPrompt,
image_data: imageData,
conversation_history: chatConversationHistory,
mood: selectedMood
};
// Make streaming request
const response = await fetch('/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Hide typing indicator
hideTypingIndicator();
// Create message element for streaming response
const assistantName = useSystemPrompt ? 'Miku' : 'LLM';
const responseDiv = addChatMessage(assistantName, '');
const contentDiv = responseDiv.querySelector('.chat-message-content');
// Read stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete SSE messages
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
const dataStr = line.slice(6);
try {
const data = JSON.parse(dataStr);
if (data.error) {
contentDiv.textContent = `❌ Error: ${data.error}`;
responseDiv.classList.add('error-message');
break;
}
if (data.content) {
fullResponse += data.content;
contentDiv.textContent = fullResponse;
// Auto-scroll
const chatMessages = document.getElementById('chat-messages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
if (data.done) {
break;
}
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
}
}
}
// If no response was received, show error
if (!fullResponse) {
contentDiv.textContent = '❌ No response received from LLM';
responseDiv.classList.add('error-message');
} else {
// Add user message to conversation history
chatConversationHistory.push({
role: "user",
content: userMessageContent
});
// Add assistant response to conversation history
chatConversationHistory.push({
role: "assistant",
content: fullResponse
});
console.log('💬 Conversation history updated:', chatConversationHistory.length, 'messages');
}
} catch (error) {
console.error('Chat error:', error);
hideTypingIndicator();
addChatMessage('Error', `Failed to send message: ${error.message}`, true);
showNotification('Failed to send message', 'error');
} finally {
// Re-enable send button
sendBtn.disabled = false;
sendBtn.innerHTML = originalBtnText;
}
}
// Helper function to read file as base64
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
</script>
</body>