feat(chat-specific): Refactor and enhance chat UI with new message display, input handling, window management, and conversation listing features

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-29 10:05:35 -07:00
parent ebfd76307b
commit eb22858ea2
4 changed files with 40 additions and 133 deletions

View file

@ -3,26 +3,9 @@ extends VBoxContainer
## Handles its own scrolling and text selection — no ScrollContainer needed.
## Supports mark_speaking() to underline the currently-playing TTS sentence.
const SELECTION_BG_COLOR := Color("#1A5C56")
const EMOTION_HEX: Dictionary = {
"happy": "#4ECDC4",
"sad": "#7B9EBD",
"angry": "#D45F5F",
"surprised": "#00E5FF",
"relaxed": "#80CBC4",
}
class _Msg:
var role: String # "user" | "assistant" | "error"
var emotion: String # assistant only
var parts: Array[String] # sentences (assistant) or [text] (user/error)
func _init(p_role: String, p_emotion: String = "") -> void:
role = p_role
emotion = p_emotion
parts = []
var message_count: int:
get:
return _messages.size()
var _rtl: RichTextLabel
var _messages: Array[_Msg] = []
@ -58,11 +41,6 @@ func setup() -> void:
add_child(_rtl)
var message_count: int:
get:
return _messages.size()
func add_user_message(text: String) -> void:
var msg := _Msg.new("user")
msg.parts.append(text)
@ -140,7 +118,8 @@ func _render() -> void:
)
)
"assistant":
var accent_hex: String = EMOTION_HEX.get(msg.emotion, UiTheme.accent.to_html(false))
var accent_color := UiTheme.emotion_colors.get(msg.emotion, UiTheme.accent) as Color
var accent_hex: String = accent_color.to_html(false)
_rtl.append_text(
(
"[color=%s]✦ Miku[/color] [color=%s]● %s[/color]\n"
@ -151,11 +130,17 @@ func _render() -> void:
var part: String = msg.parts[j]
if i == _speaking_msg and j == _speaking_part:
_rtl.append_text(
"[u][color=%s]%s[/color][/u]" % [UiTheme.text_primary.to_html(false), _escape(part)]
(
"[u][color=%s]%s[/color][/u]"
% [UiTheme.text_primary.to_html(false), _escape(part)]
)
)
else:
_rtl.append_text(
"[color=%s]%s[/color]" % [UiTheme.text_primary.to_html(false), _escape(part)]
(
"[color=%s]%s[/color]"
% [UiTheme.text_primary.to_html(false), _escape(part)]
)
)
"error":
_rtl.append_text(
@ -185,7 +170,8 @@ func _append_user_block(text: String) -> void:
func _append_miku_header(emotion: String) -> void:
if _rtl.get_parsed_text().length() > 0:
_rtl.append_text("\n\n")
var accent_hex: String = EMOTION_HEX.get(emotion, UiTheme.accent.to_html(false))
var accent_color := UiTheme.emotion_colors.get(emotion, UiTheme.accent) as Color
var accent_hex: String = accent_color.to_html(false)
_rtl.append_text(
(
"[color=%s]✦ Miku[/color] [color=%s]● %s[/color]\n"
@ -210,3 +196,14 @@ func _do_scroll() -> void:
func _escape(text: String) -> String:
return text.replace("[", "[lb]")
class _Msg:
var role: String # "user" | "assistant" | "error"
var emotion: String # assistant only
var parts: Array[String] # sentences (assistant) or [text] (user/error)
func _init(p_role: String, p_emotion: String = "") -> void:
role = p_role
emotion = p_emotion
parts = []

View file

@ -1,85 +1,2 @@
extends PanelContainer
## Chat input bar — text field with send button, Miku-themed.
signal message_submitted(text: String)
var _input: LineEdit
func setup() -> void:
var style := StyleBoxFlat.new()
style.bg_color = UiTheme.bg_panel
style.corner_radius_bottom_left = 10
style.corner_radius_bottom_right = 10
add_theme_stylebox_override("panel", style)
custom_minimum_size.y = 52
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
add_child(margin)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 8)
margin.add_child(hbox)
_input = LineEdit.new()
_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_input.placeholder_text = "Type a message..."
_input.add_theme_color_override("font_color", UiTheme.text_primary)
_input.add_theme_color_override("font_placeholder_color", UiTheme.text_muted)
_input.add_theme_color_override("caret_color", UiTheme.accent)
var input_normal := StyleBoxFlat.new()
input_normal.bg_color = UiTheme.input_bg
input_normal.set_border_width_all(1)
input_normal.border_color = UiTheme.border
input_normal.set_corner_radius_all(6)
input_normal.content_margin_left = 10
input_normal.content_margin_right = 10
input_normal.content_margin_top = 6
input_normal.content_margin_bottom = 6
_input.add_theme_stylebox_override("normal", input_normal)
var input_focus := input_normal.duplicate() as StyleBoxFlat
input_focus.border_color = UiTheme.accent
_input.add_theme_stylebox_override("focus", input_focus)
_input.text_submitted.connect(_on_text_submitted)
hbox.add_child(_input)
var send_btn := Button.new()
send_btn.text = ""
send_btn.custom_minimum_size = Vector2(40, 36)
send_btn.add_theme_color_override("font_color", UiTheme.bg_dark)
send_btn.add_theme_font_size_override("font_size", 18)
var btn_normal := StyleBoxFlat.new()
btn_normal.bg_color = UiTheme.accent
btn_normal.set_corner_radius_all(6)
send_btn.add_theme_stylebox_override("normal", btn_normal)
var btn_hover := btn_normal.duplicate() as StyleBoxFlat
btn_hover.bg_color = UiTheme.accent_hover()
send_btn.add_theme_stylebox_override("hover", btn_hover)
var btn_pressed := btn_normal.duplicate() as StyleBoxFlat
btn_pressed.bg_color = UiTheme.accent_press()
send_btn.add_theme_stylebox_override("pressed", btn_pressed)
send_btn.pressed.connect(_on_send_pressed)
hbox.add_child(send_btn)
func _on_text_submitted(text: String) -> void:
text = text.strip_edges()
if text.is_empty():
return
_input.text = ""
message_submitted.emit(text)
_input.grab_focus()
func _on_send_pressed() -> void:
_on_text_submitted(_input.text)
extends "res://addons/godot-ui/chat/input_bar.gd"
## Chat input bar — delegates to godot-ui InputBar component.

View file

@ -46,6 +46,7 @@ func _ready() -> void:
min_size = Vector2i(340, 480)
size = Vector2i(380, 560)
super._ready()
setup()
func _build_ui() -> void:
@ -64,7 +65,7 @@ func _build_ui() -> void:
root.add_theme_constant_override("separation", 0)
bg.add_child(root)
root.add_child(_build_title_bar())
root.add_child(_build_chat_title_bar())
root.add_child(_build_divider())
_conversation_list = ConversationListScript.new()
@ -94,7 +95,7 @@ func _build_ui() -> void:
root.add_child(_input_bar)
func _build_title_bar() -> Control:
func _build_chat_title_bar() -> Control:
_status_label = Label.new()
_status_label.text = ""
_status_label.add_theme_color_override("font_color", UiTheme.text_muted)
@ -120,7 +121,7 @@ func _build_title_bar() -> Control:
new_btn.add_theme_font_size_override("font_size", 18)
new_btn.pressed.connect(_on_new_conversation)
return _build_panel_title_bar("✦ MIKU", [_status_label, list_btn, new_btn])
return _build_title_bar("✦ MIKU", [_status_label, list_btn, new_btn])
func replay_messages(messages: Array[Dictionary]) -> void:

View file

@ -2,14 +2,6 @@ extends VBoxContainer
## Collapsible conversation history list for the chat window.
## Shows recent conversations, highlights the active one, emits switch signals.
const BG_DARK := Color("#0D1117")
const BG_PANEL := Color("#111822")
const BG_HOVER := Color("#162230")
const MIKU_TEAL := Color("#39C5BB")
const TEXT_PRIMARY := Color("#E8F4F3")
const TEXT_MUTED := Color("#6B8E8B")
const BORDER_COLOR := Color("#1A3330")
const MAX_VISIBLE: int = 10
var _item_container: VBoxContainer
@ -22,9 +14,9 @@ func setup() -> void:
var wrapper := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = BG_PANEL
style.bg_color = UiTheme.bg_panel
style.set_border_width_all(1)
style.border_color = BORDER_COLOR
style.border_color = UiTheme.border
style.content_margin_left = 6
style.content_margin_right = 6
style.content_margin_top = 6
@ -74,7 +66,7 @@ func refresh() -> void:
if conversations.is_empty():
var empty_label := Label.new()
empty_label.text = "No conversations yet"
empty_label.add_theme_color_override("font_color", TEXT_MUTED)
empty_label.add_theme_color_override("font_color", UiTheme.text_muted)
empty_label.add_theme_font_size_override("font_size", 11)
_item_container.add_child(empty_label)
return
@ -109,16 +101,16 @@ func _build_item(
btn.text = display_title + suffix
if is_active:
btn.add_theme_color_override("font_color", MIKU_TEAL)
btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
btn.add_theme_color_override("font_color", UiTheme.accent)
btn.add_theme_color_override("font_hover_color", UiTheme.accent)
else:
btn.add_theme_color_override("font_color", TEXT_PRIMARY)
btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
btn.add_theme_color_override("font_color", UiTheme.text_primary)
btn.add_theme_color_override("font_hover_color", UiTheme.accent)
btn.add_theme_font_size_override("font_size", 12)
var hover_style := StyleBoxFlat.new()
hover_style.bg_color = BG_HOVER
hover_style.bg_color = UiTheme.item_hover
hover_style.set_corner_radius_all(4)
btn.add_theme_stylebox_override("hover", hover_style)