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:
182
bot/api.py
182
bot/api.py
@@ -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)
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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︀︀\n︀︀Estimated in\\-stock date\\: 09/2026\n︀︀\n︀︀Pre\\-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, who’s rocking off her most colorful socks in this adorable Noodle Stopper figure\\. Snag her now at GOODSMILE ONLINE SHOP US\\!\n︀︀\n︀︀Shop\\: [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︀︀\n︀︀Shop\\: [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︀︀\n︀︀Estimated in\\-stock date\\: 10/2026\n︀︀\n︀︀Pre\\-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 |
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user