Richard Shiue 34d2b7f24e
chore: improve local ai settings page (#7651)
* chore: improve local ai settings page

* chore: move local to top in select list

* chore: improve copy of missing model

* chore: remove green background and add progress indicator

* chore: change text color
2025-03-31 12:05:11 +08:00

756 lines
17 KiB
Rust

use af_plugin::core::plugin::RunningState;
use std::collections::HashMap;
use crate::local_ai::controller::LocalAISetting;
use crate::local_ai::resource::PendingResource;
use flowy_ai_pub::cloud::{
AIModel, ChatMessage, ChatMessageMetadata, ChatMessageType, CompletionMessage, LLMModel,
OutputContent, OutputLayout, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion,
ResponseFormat,
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use lib_infra::validator_fn::required_not_empty_str;
use validator::Validate;
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ChatId {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub value: String,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct ChatInfoPB {
#[pb(index = 1)]
pub chat_id: String,
#[pb(index = 2)]
pub files: Vec<FilePB>,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct FilePB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub name: String,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct SendChatPayloadPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
#[pb(index = 2)]
#[validate(custom(function = "required_not_empty_str"))]
pub message: String,
#[pb(index = 3)]
pub message_type: ChatMessageTypePB,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct StreamChatPayloadPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
#[pb(index = 2)]
#[validate(custom(function = "required_not_empty_str"))]
pub message: String,
#[pb(index = 3)]
pub message_type: ChatMessageTypePB,
#[pb(index = 4)]
pub answer_stream_port: i64,
#[pb(index = 5)]
pub question_stream_port: i64,
#[pb(index = 6, one_of)]
pub format: Option<PredefinedFormatPB>,
#[pb(index = 7)]
pub metadata: Vec<ChatMessageMetaPB>,
}
#[derive(Default, Debug)]
pub struct StreamMessageParams {
pub chat_id: String,
pub message: String,
pub message_type: ChatMessageType,
pub answer_stream_port: i64,
pub question_stream_port: i64,
pub format: Option<PredefinedFormatPB>,
pub metadata: Vec<ChatMessageMetadata>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct RegenerateResponsePB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
#[pb(index = 2)]
pub answer_message_id: i64,
#[pb(index = 3)]
pub answer_stream_port: i64,
#[pb(index = 4, one_of)]
pub format: Option<PredefinedFormatPB>,
#[pb(index = 5, one_of)]
pub model: Option<AIModelPB>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ChatMessageMetaPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub name: String,
#[pb(index = 3)]
pub data: String,
#[pb(index = 4)]
pub loader_type: ContextLoaderTypePB,
#[pb(index = 5)]
pub source: String,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum ContextLoaderTypePB {
#[default]
UnknownLoaderType = 0,
Txt = 1,
Markdown = 2,
PDF = 3,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct StopStreamPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum ChatMessageTypePB {
#[default]
System = 0,
User = 1,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct LoadPrevChatMessagePB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
#[pb(index = 2)]
pub limit: i64,
#[pb(index = 4, one_of)]
pub before_message_id: Option<i64>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct LoadNextChatMessagePB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
#[pb(index = 2)]
pub limit: i64,
#[pb(index = 4, one_of)]
pub after_message_id: Option<i64>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ChatMessageListPB {
#[pb(index = 1)]
pub has_more: bool,
#[pb(index = 2)]
pub messages: Vec<ChatMessagePB>,
/// If the total number of messages is 0, then the total number of messages is unknown.
#[pb(index = 3)]
pub total: i64,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct ServerAvailableModelsPB {
#[pb(index = 1)]
pub models: Vec<AvailableModelPB>,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct AvailableModelPB {
#[pb(index = 1)]
pub name: String,
#[pb(index = 2)]
pub is_default: bool,
#[pb(index = 3)]
pub desc: String,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct AvailableModelsQueryPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub source: String,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct UpdateSelectedModelPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub source: String,
#[pb(index = 2)]
pub selected_model: AIModelPB,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct AvailableModelsPB {
#[pb(index = 1)]
pub models: Vec<AIModelPB>,
#[pb(index = 2)]
pub selected_model: AIModelPB,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct AIModelPB {
#[pb(index = 1)]
pub name: String,
#[pb(index = 2)]
pub is_local: bool,
#[pb(index = 3)]
pub desc: String,
}
impl From<AIModel> for AIModelPB {
fn from(model: AIModel) -> Self {
Self {
name: model.name,
is_local: model.is_local,
desc: model.desc,
}
}
}
impl From<AIModelPB> for AIModel {
fn from(value: AIModelPB) -> Self {
AIModel {
name: value.name,
is_local: value.is_local,
desc: value.desc,
}
}
}
impl From<RepeatedChatMessage> for ChatMessageListPB {
fn from(repeated_chat_message: RepeatedChatMessage) -> Self {
let messages = repeated_chat_message
.messages
.into_iter()
.map(ChatMessagePB::from)
.collect();
ChatMessageListPB {
has_more: repeated_chat_message.has_more,
messages,
total: repeated_chat_message.total,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessagePB {
#[pb(index = 1)]
pub message_id: i64,
#[pb(index = 2)]
pub content: String,
#[pb(index = 3)]
pub created_at: i64,
#[pb(index = 4)]
pub author_type: i64,
#[pb(index = 5)]
pub author_id: String,
#[pb(index = 6, one_of)]
pub reply_message_id: Option<i64>,
#[pb(index = 7, one_of)]
pub metadata: Option<String>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessageErrorPB {
#[pb(index = 1)]
pub chat_id: String,
#[pb(index = 2)]
pub error_message: String,
}
impl From<ChatMessage> for ChatMessagePB {
fn from(chat_message: ChatMessage) -> Self {
ChatMessagePB {
message_id: chat_message.message_id,
content: chat_message.content,
created_at: chat_message.created_at.timestamp(),
author_type: chat_message.author.author_type as i64,
author_id: chat_message.author.author_id.to_string(),
reply_message_id: None,
metadata: Some(serde_json::to_string(&chat_message.meta_data).unwrap_or_default()),
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedChatMessagePB {
#[pb(index = 1)]
items: Vec<ChatMessagePB>,
}
impl From<Vec<ChatMessage>> for RepeatedChatMessagePB {
fn from(messages: Vec<ChatMessage>) -> Self {
RepeatedChatMessagePB {
items: messages.into_iter().map(ChatMessagePB::from).collect(),
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessageIdPB {
#[pb(index = 1)]
pub chat_id: String,
#[pb(index = 2)]
pub message_id: i64,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RelatedQuestionPB {
#[pb(index = 1)]
pub content: String,
}
impl From<RelatedQuestion> for RelatedQuestionPB {
fn from(value: RelatedQuestion) -> Self {
RelatedQuestionPB {
content: value.content,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedRelatedQuestionPB {
#[pb(index = 1)]
pub message_id: i64,
#[pb(index = 2)]
pub items: Vec<RelatedQuestionPB>,
}
impl From<RepeatedRelatedQuestion> for RepeatedRelatedQuestionPB {
fn from(value: RepeatedRelatedQuestion) -> Self {
RepeatedRelatedQuestionPB {
message_id: value.message_id,
items: value
.items
.into_iter()
.map(RelatedQuestionPB::from)
.collect(),
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct LLMModelPB {
#[pb(index = 1)]
pub llm_id: i64,
#[pb(index = 2)]
pub embedding_model: String,
#[pb(index = 3)]
pub chat_model: String,
#[pb(index = 4)]
pub requirement: String,
#[pb(index = 5)]
pub file_size: i64,
}
impl From<LLMModel> for LLMModelPB {
fn from(value: LLMModel) -> Self {
LLMModelPB {
llm_id: value.llm_id,
embedding_model: value.embedding_model.name,
chat_model: value.chat_model.name,
requirement: value.chat_model.requirements,
file_size: value.chat_model.file_size,
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct CompleteTextPB {
#[pb(index = 1)]
pub text: String,
#[pb(index = 2)]
pub completion_type: CompletionTypePB,
#[pb(index = 3, one_of)]
pub format: Option<PredefinedFormatPB>,
#[pb(index = 4)]
pub stream_port: i64,
#[pb(index = 5)]
pub object_id: String,
#[pb(index = 6)]
pub rag_ids: Vec<String>,
#[pb(index = 7)]
pub history: Vec<CompletionRecordPB>,
#[pb(index = 8, one_of)]
pub custom_prompt: Option<String>,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct CompleteTextTaskPB {
#[pb(index = 1)]
pub task_id: String,
}
#[derive(Clone, Debug, ProtoBuf_Enum, Default)]
pub enum CompletionTypePB {
#[default]
UserQuestion = 0,
ExplainSelected = 1,
ContinueWriting = 2,
SpellingAndGrammar = 3,
ImproveWriting = 4,
MakeShorter = 5,
MakeLonger = 6,
CustomPrompt = 7,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct CompletionRecordPB {
#[pb(index = 1)]
pub role: ChatMessageTypePB,
#[pb(index = 2)]
pub content: String,
}
impl From<&CompletionRecordPB> for CompletionMessage {
fn from(value: &CompletionRecordPB) -> Self {
CompletionMessage {
role: match value.role {
// Coerce ChatMessageTypePB::System to AI
ChatMessageTypePB::System => "ai".to_string(),
ChatMessageTypePB::User => "human".to_string(),
},
content: value.content.clone(),
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct ChatStatePB {
#[pb(index = 1)]
pub model_type: ModelTypePB,
#[pb(index = 2)]
pub available: bool,
}
#[derive(Clone, Debug, ProtoBuf_Enum, Default)]
pub enum ModelTypePB {
LocalAI = 0,
#[default]
RemoteAI = 1,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ChatFilePB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub file_path: String,
#[pb(index = 2)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct LocalModelStatePB {
#[pb(index = 1)]
pub model_name: String,
#[pb(index = 2)]
pub model_size: String,
#[pb(index = 3)]
pub need_download: bool,
#[pb(index = 4)]
pub requirements: String,
#[pb(index = 5)]
pub is_downloading: bool,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct PendingResourcePB {
#[pb(index = 1)]
pub name: String,
#[pb(index = 2)]
pub file_size: String,
#[pb(index = 3)]
pub requirements: String,
#[pb(index = 4)]
pub res_type: PendingResourceTypePB,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum PendingResourceTypePB {
#[default]
LocalAIAppRes = 0,
ModelRes = 1,
}
impl From<PendingResource> for PendingResourceTypePB {
fn from(value: PendingResource) -> Self {
match value {
PendingResource::PluginExecutableNotReady { .. } => PendingResourceTypePB::LocalAIAppRes,
_ => PendingResourceTypePB::ModelRes,
}
}
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum RunningStatePB {
#[default]
ReadyToRun = 0,
Connecting = 1,
Connected = 2,
Running = 3,
Stopped = 4,
}
impl From<RunningState> for RunningStatePB {
fn from(value: RunningState) -> Self {
match value {
RunningState::ReadyToConnect => RunningStatePB::ReadyToRun,
RunningState::Connecting => RunningStatePB::Connecting,
RunningState::Connected { .. } => RunningStatePB::Connected,
RunningState::Running { .. } => RunningStatePB::Running,
RunningState::Stopped { .. } => RunningStatePB::Stopped,
RunningState::UnexpectedStop { .. } => RunningStatePB::Stopped,
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct LocalAIPB {
#[pb(index = 1)]
pub enabled: bool,
#[pb(index = 2, one_of)]
pub lack_of_resource: Option<LackOfAIResourcePB>,
#[pb(index = 3)]
pub state: RunningStatePB,
#[pb(index = 4, one_of)]
pub plugin_version: Option<String>,
#[pb(index = 5)]
pub plugin_downloaded: bool,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct CreateChatContextPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub content_type: String,
#[pb(index = 2)]
#[validate(custom(function = "required_not_empty_str"))]
pub text: String,
#[pb(index = 3)]
pub metadata: HashMap<String, String>,
#[pb(index = 4)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_id: String,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct ChatSettingsPB {
#[pb(index = 1)]
pub rag_ids: Vec<String>,
}
#[derive(Default, ProtoBuf, Clone, Debug, Validate)]
pub struct UpdateChatSettingsPB {
#[pb(index = 1)]
#[validate(nested)]
pub chat_id: ChatId,
#[pb(index = 2)]
pub rag_ids: Vec<String>,
#[pb(index = 3)]
pub chat_model: String,
}
#[derive(Debug, Default, Clone, ProtoBuf)]
pub struct PredefinedFormatPB {
#[pb(index = 1)]
pub image_format: ResponseImageFormatPB,
#[pb(index = 2, one_of)]
pub text_format: Option<ResponseTextFormatPB>,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum)]
pub enum ResponseImageFormatPB {
#[default]
TextOnly = 0,
ImageOnly = 1,
TextAndImage = 2,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum)]
pub enum ResponseTextFormatPB {
#[default]
Paragraph = 0,
BulletedList = 1,
NumberedList = 2,
Table = 3,
}
impl From<PredefinedFormatPB> for ResponseFormat {
fn from(value: PredefinedFormatPB) -> Self {
Self {
output_layout: match value.text_format {
Some(format) => match format {
ResponseTextFormatPB::Paragraph => OutputLayout::Paragraph,
ResponseTextFormatPB::BulletedList => OutputLayout::BulletList,
ResponseTextFormatPB::NumberedList => OutputLayout::NumberedList,
ResponseTextFormatPB::Table => OutputLayout::SimpleTable,
},
None => OutputLayout::Paragraph,
},
output_content: match value.image_format {
ResponseImageFormatPB::TextOnly => OutputContent::TEXT,
ResponseImageFormatPB::ImageOnly => OutputContent::IMAGE,
ResponseImageFormatPB::TextAndImage => OutputContent::RichTextImage,
},
output_content_metadata: None,
}
}
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct LocalAISettingPB {
#[pb(index = 1)]
#[validate(custom(function = "required_not_empty_str"))]
pub server_url: String,
#[pb(index = 2)]
#[validate(custom(function = "required_not_empty_str"))]
pub chat_model_name: String,
#[pb(index = 3)]
#[validate(custom(function = "required_not_empty_str"))]
pub embedding_model_name: String,
}
impl From<LocalAISetting> for LocalAISettingPB {
fn from(value: LocalAISetting) -> Self {
LocalAISettingPB {
server_url: value.ollama_server_url,
chat_model_name: value.chat_model_name,
embedding_model_name: value.embedding_model_name,
}
}
}
impl From<LocalAISettingPB> for LocalAISetting {
fn from(value: LocalAISettingPB) -> Self {
LocalAISetting {
ollama_server_url: value.server_url,
chat_model_name: value.chat_model_name,
embedding_model_name: value.embedding_model_name,
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct LackOfAIResourcePB {
#[pb(index = 1)]
pub resource_type: LackOfAIResourceTypePB,
#[pb(index = 2)]
pub missing_model_names: Vec<String>,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum)]
pub enum LackOfAIResourceTypePB {
#[default]
PluginExecutableNotReady = 0,
OllamaServerNotReady = 1,
MissingModel = 2,
}
impl From<PendingResource> for LackOfAIResourcePB {
fn from(value: PendingResource) -> Self {
match value {
PendingResource::PluginExecutableNotReady => Self {
resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady,
missing_model_names: vec![],
},
PendingResource::OllamaServerNotReady => Self {
resource_type: LackOfAIResourceTypePB::OllamaServerNotReady,
missing_model_names: vec![],
},
PendingResource::MissingModel(model_name) => Self {
resource_type: LackOfAIResourceTypePB::MissingModel,
missing_model_names: vec![model_name],
},
}
}
}