feat(chat): added chat interface and connection to ollama #10

Merged
sharang merged 6 commits from feat/CAI-20 into main 2026-02-20 19:40:26 +00:00
4 changed files with 188 additions and 2 deletions
Showing only changes of commit 7bff029194 - Show all commits

View File

@@ -1884,6 +1884,44 @@ h6 {
color: var(--accent);
}
/* -- Chat Action Bar -- */
.chat-action-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 24px 0;
background-color: var(--bg-sidebar);
}
.chat-action-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: none;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.15s ease;
}
.chat-action-btn:hover:not(:disabled) {
color: var(--text-primary);
background-color: var(--bg-card);
border-color: var(--border-secondary);
}
.chat-action-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.chat-action-label {
font-family: 'Inter', sans-serif;
}
/* -- Chat Input Bar -- */
.chat-input-bar {
display: flex;

View File

@@ -0,0 +1,65 @@
use dioxus::prelude::*;
use dioxus_free_icons::icons::fa_solid_icons::{FaCopy, FaPenToSquare, FaShareNodes};
/// Action bar displayed above the chat input with copy, share, and edit buttons.
///
/// Only visible when there is at least one message in the conversation.
///
/// # Arguments
///
/// * `on_copy` - Copies the last assistant response to the clipboard
/// * `on_share` - Copies the full conversation as text to the clipboard
/// * `on_edit` - Places the last user message back in the input for editing
/// * `has_messages` - Whether any messages exist (hides the bar when empty)
/// * `has_assistant_message` - Whether an assistant message exists (disables copy if not)
/// * `has_user_message` - Whether a user message exists (disables edit if not)
#[component]
pub fn ChatActionBar(
on_copy: EventHandler<()>,
on_share: EventHandler<()>,
on_edit: EventHandler<()>,
has_messages: bool,
has_assistant_message: bool,
has_user_message: bool,
) -> Element {
if !has_messages {
return rsx! {};
}
rsx! {
div { class: "chat-action-bar",
button {
class: "chat-action-btn",
disabled: !has_assistant_message,
title: "Copy last response",
onclick: move |_| on_copy.call(()),
dioxus_free_icons::Icon {
icon: FaCopy,
width: 14, height: 14,
}
span { class: "chat-action-label", "Copy" }
}
button {
class: "chat-action-btn",
title: "Copy conversation",
onclick: move |_| on_share.call(()),
dioxus_free_icons::Icon {
icon: FaShareNodes,
width: 14, height: 14,
}
span { class: "chat-action-label", "Share" }
}
button {
class: "chat-action-btn",
disabled: !has_user_message,
title: "Edit last message",
onclick: move |_| on_edit.call(()),
dioxus_free_icons::Icon {
icon: FaPenToSquare,
width: 14, height: 14,
}
span { class: "chat-action-label", "Edit" }
}
}
}
}

View File

@@ -1,6 +1,7 @@
mod app_shell;
mod article_detail;
mod card;
mod chat_action_bar;
mod chat_bubble;
mod chat_input_bar;
mod chat_message_list;
@@ -20,6 +21,7 @@ mod tool_card;
pub use app_shell::*;
pub use article_detail::*;
pub use card::*;
pub use chat_action_bar::*;
pub use chat_bubble::*;
pub use chat_input_bar::*;
pub use chat_message_list::*;

View File

@@ -1,4 +1,6 @@
use crate::components::{ChatInputBar, ChatMessageList, ChatModelSelector, ChatSidebar};
use crate::components::{
ChatActionBar, ChatInputBar, ChatMessageList, ChatModelSelector, ChatSidebar,
};
use crate::infrastructure::chat::{
chat_complete, create_chat_session, delete_chat_session, list_chat_messages,
list_chat_sessions, rename_chat_session, save_chat_message,
@@ -16,7 +18,7 @@ pub fn ChatPage() -> Element {
// ---- Signals ----
let mut active_session_id: Signal<Option<String>> = use_signal(|| None);
let mut messages: Signal<Vec<ChatMessage>> = use_signal(Vec::new);
let input_text: Signal<String> = use_signal(String::new);
let mut input_text: Signal<String> = use_signal(String::new);
let mut is_streaming: Signal<bool> = use_signal(|| false);
let mut streaming_content: Signal<String> = use_signal(String::new);
let mut selected_model: Signal<String> = use_signal(String::new);
@@ -202,6 +204,77 @@ pub fn ChatPage() -> Element {
});
};
// ---- Action bar state ----
let has_messages = !messages.read().is_empty();
let has_assistant_message = messages
.read()
.iter()
.any(|m| m.role == ChatRole::Assistant);
let has_user_message = messages.read().iter().any(|m| m.role == ChatRole::User);
// Copy last assistant response to clipboard
let on_copy = move |_: ()| {
#[cfg(feature = "web")]
{
let last_assistant = messages
.read()
.iter()
.rev()
.find(|m| m.role == ChatRole::Assistant)
.map(|m| m.content.clone());
if let Some(text) = last_assistant {
if let Some(window) = web_sys::window() {
let clipboard = window.navigator().clipboard();
let _ = clipboard.write_text(&text);
}
}
}
};
// Copy full conversation as text to clipboard
let on_share = move |_: ()| {
#[cfg(feature = "web")]
{
let text: String = messages
.read()
.iter()
.filter(|m| m.role != ChatRole::System)
.map(|m| {
let label = match m.role {
ChatRole::User => "You",
ChatRole::Assistant => "Assistant",
ChatRole::System => "System",
};
format!("{label}:\n{}\n", m.content)
})
.collect::<Vec<_>>()
.join("\n");
if let Some(window) = web_sys::window() {
let clipboard = window.navigator().clipboard();
let _ = clipboard.write_text(&text);
}
}
};
// Edit last user message: remove it and place text back in input
let on_edit = move |_: ()| {
let last_user = messages
.read()
.iter()
.rev()
.find(|m| m.role == ChatRole::User)
.map(|m| m.content.clone());
if let Some(text) = last_user {
// Remove the last user message (and any assistant reply after it)
let mut msgs = messages.read().clone();
if let Some(pos) = msgs.iter().rposition(|m| m.role == ChatRole::User) {
msgs.truncate(pos);
messages.set(msgs);
}
input_text.set(text);
}
};
// Scroll to bottom when messages or streaming content changes
let msg_count = messages.read().len();
let stream_len = streaming_content.read().len();
@@ -244,6 +317,14 @@ pub fn ChatPage() -> Element {
streaming_content: streaming_content.read().clone(),
is_streaming: *is_streaming.read(),
}
ChatActionBar {
on_copy: on_copy,
on_share: on_share,
on_edit: on_edit,
has_messages: has_messages,
has_assistant_message: has_assistant_message,
has_user_message: has_user_message,
}
ChatInputBar {
input_text: input_text,
on_send: on_send,