chore: delete unused code files and dependencies (#841)

Mainly delete unused webSocket content, and delete other unused code files and dependencies
This commit is contained in:
BiggerRain
2025-07-29 13:02:28 +08:00
committed by GitHub
parent 99144950d9
commit 232166eb89
41 changed files with 3 additions and 1798 deletions

2
.env
View File

@@ -1,5 +1,3 @@
COCO_SERVER_URL=http://localhost:9000 #https://coco.infini.cloud #http://localhost:9000
COCO_WEBSOCKET_URL=ws://localhost:9000/ws #wss://coco.infini.cloud/ws #ws://localhost:9000/ws
#TAURI_DEV_HOST=0.0.0.0

View File

@@ -24,6 +24,7 @@ Information about release notes of Coco App is provided here.
- refactor: split query_coco_fusion() #836
- chore: web component loading font icon #838
- chore: delete unused code files and dependencies #841
## 0.7.1 (2025-07-27)

View File

@@ -31,7 +31,6 @@
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.1",
"@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2",
"@tauri-apps/plugin-websocket": "~2.3.0",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"@wavesurfer/react": "^1.0.11",
"ahooks": "^3.8.4",

10
pnpm-lock.yaml generated
View File

@@ -47,9 +47,6 @@ importers:
'@tauri-apps/plugin-updater':
specifier: github:infinilabs/tauri-plugin-updater#v2
version: https://codeload.github.com/infinilabs/tauri-plugin-updater/tar.gz/358e689c65e9943b53eff50bcb9dfd5b1cfc4072
'@tauri-apps/plugin-websocket':
specifier: ~2.3.0
version: 2.3.0
'@tauri-apps/plugin-window':
specifier: 2.0.0-alpha.1
version: 2.0.0-alpha.1
@@ -1261,9 +1258,6 @@ packages:
resolution: {tarball: https://codeload.github.com/infinilabs/tauri-plugin-updater/tar.gz/358e689c65e9943b53eff50bcb9dfd5b1cfc4072}
version: 2.7.1
'@tauri-apps/plugin-websocket@2.3.0':
resolution: {integrity: sha512-eAwRGe3tnqDeQYE0wq4g1PUKbam9tYvlC4uP/au12Y/z7MP4lrS4ylv+aoZ5Ly+hTlBdi7hDkhHomwF/UeBesA==}
'@tauri-apps/plugin-window@2.0.0-alpha.1':
resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==}
@@ -4643,10 +4637,6 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-websocket@2.3.0':
dependencies:
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-window@2.0.0-alpha.1':
dependencies:
'@tauri-apps/api': 2.0.0-alpha.6

57
src-tauri/Cargo.lock generated
View File

@@ -900,13 +900,12 @@ dependencies = [
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-websocket",
"tauri-plugin-windows-version",
"thiserror 1.0.69",
"tokio",
"tokio-native-tls",
"tokio-stream",
"tokio-tungstenite 0.20.1",
"tokio-tungstenite",
"tokio-util",
"tungstenite 0.24.0",
"url",
@@ -6556,25 +6555,6 @@ dependencies = [
"zip 2.6.1",
]
[[package]]
name = "tauri-plugin-websocket"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3ac71aec5fb0ae5441e830cd075b1cbed49ac3d39cb975a4894ea8fa2e62b9"
dependencies = [
"futures-util",
"http 1.3.1",
"log",
"rand 0.8.5",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"tokio",
"tokio-tungstenite 0.26.2",
]
[[package]]
name = "tauri-plugin-windows-version"
version = "2.0.0"
@@ -6909,22 +6889,6 @@ dependencies = [
"tungstenite 0.20.1",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite 0.26.2",
"webpki-roots 0.26.11",
]
[[package]]
name = "tokio-util"
version = "0.7.15"
@@ -7131,25 +7095,6 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"

View File

@@ -51,7 +51,6 @@ serde = { version = "1", features = ["derive"] }
# see: https://docs.rs/serde_json/latest/serde_json/struct.Number.html#method.from_u128
serde_json = { version = "1", features = ["arbitrary_precision", "preserve_order"] }
tauri-plugin-http = "2"
tauri-plugin-websocket = "2"
tauri-plugin-deep-link = "2.0.0"
tauri-plugin-store = "2.2.0"
tauri-plugin-os = "2"

View File

@@ -37,9 +37,6 @@
"http:allow-fetch-cancel",
"http:allow-fetch-read-body",
"http:allow-fetch-send",
"websocket:default",
"websocket:allow-connect",
"websocket:allow-send",
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled",

View File

@@ -1,5 +1,5 @@
use crate::common::assistant::ChatRequestMessage;
use crate::common::http::{GetResponse, convert_query_params_to_strings};
use crate::common::http::convert_query_params_to_strings;
use crate::common::register::SearchSourceRegistry;
use crate::server::http_client::HttpClient;
use crate::{common, server::servers::COCO_SERVERS};
@@ -111,54 +111,6 @@ pub async fn cancel_session_chat<R: Runtime>(
common::http::get_response_body_text(response).await
}
#[tauri::command]
pub async fn new_chat<R: Runtime>(
_app_handle: AppHandle<R>,
server_id: String,
websocket_id: String,
message: String,
query_params: Option<HashMap<String, Value>>,
) -> Result<GetResponse, String> {
let body = if !message.is_empty() {
let message = ChatRequestMessage {
message: Some(message),
};
Some(
serde_json::to_string(&message)
.map_err(|e| format!("Failed to serialize message: {}", e))?
.into(),
)
} else {
None
};
let mut headers = HashMap::new();
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
let response = HttpClient::advanced_post(
&server_id,
"/chat/_new",
Some(headers),
convert_query_params_to_strings(query_params),
body,
)
.await
.map_err(|e| format!("Error sending message: {}", e))?;
let body_text = common::http::get_response_body_text(response).await?;
log::debug!("New chat response: {}", &body_text);
let chat_response: GetResponse = serde_json::from_str(&body_text)
.map_err(|e| format!("Failed to parse response JSON: {}", e))?;
if chat_response.result != "created" {
return Err(format!("Unexpected result: {}", chat_response.result));
}
Ok(chat_response)
}
#[tauri::command]
pub async fn chat_create<R: Runtime>(
app_handle: AppHandle<R>,
@@ -222,37 +174,6 @@ pub async fn chat_create<R: Runtime>(
Ok(())
}
#[tauri::command]
pub async fn send_message<R: Runtime>(
_app_handle: AppHandle<R>,
server_id: String,
websocket_id: String,
session_id: String,
message: String,
query_params: Option<HashMap<String, Value>>, //search,deep_thinking
) -> Result<String, String> {
let path = format!("/chat/{}/_send", session_id);
let msg = ChatRequestMessage {
message: Some(message),
};
let mut headers = HashMap::new();
headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
let body = reqwest::Body::from(serde_json::to_string(&msg).unwrap());
let response = HttpClient::advanced_post(
&server_id,
path.as_str(),
Some(headers),
convert_query_params_to_strings(query_params),
Some(body),
)
.await
.map_err(|e| format!("Error cancel session: {}", e))?;
common::http::get_response_body_text(response).await
}
#[tauri::command]
pub async fn chat_chat<R: Runtime>(
app_handle: AppHandle<R>,

View File

@@ -130,9 +130,7 @@ pub fn run() {
server::connector::get_connectors_by_server,
search::query_coco_fusion,
assistant::chat_history,
assistant::new_chat,
assistant::chat_create,
assistant::send_message,
assistant::chat_chat,
assistant::session_chat_history,
assistant::open_session_chat,
@@ -145,8 +143,6 @@ pub fn run() {
assistant::assistant_get_multi,
// server::get_coco_server_datasources,
// server::get_coco_server_connectors,
server::websocket::connect_to_server,
server::websocket::disconnect,
get_app_search_source,
server::attachment::upload_attachment,
server::attachment::get_attachment,
@@ -201,7 +197,6 @@ pub fn run() {
let registry = SearchSourceRegistry::default();
app.manage(registry); // Store registry in Tauri's app state
app.manage(server::websocket::WebSocketManager::default());
// This has to be called before initializing extensions as doing that
// requires access to the shortcut store, which will be set by this

View File

@@ -11,4 +11,3 @@ pub mod servers;
pub mod synthesize;
pub mod system_settings;
pub mod transcription;
pub mod websocket;

View File

@@ -1,172 +0,0 @@
use crate::server::servers::{get_server_by_id, get_server_token};
use futures::StreamExt;
use std::collections::HashMap;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, Runtime};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, mpsc};
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::{Connector, connect_async_tls_with_config};
#[derive(Default)]
pub struct WebSocketManager {
connections: Arc<Mutex<HashMap<String, Arc<WebSocketInstance>>>>,
}
struct WebSocketInstance {
ws_connection: Mutex<WebSocketStream<MaybeTlsStream<TcpStream>>>, // No need to lock the entire map
cancel_tx: mpsc::Sender<()>,
}
fn convert_to_websocket(endpoint: &str) -> Result<String, String> {
let url = url::Url::parse(endpoint).map_err(|e| format!("Invalid URL: {}", e))?;
let ws_protocol = if url.scheme() == "https" {
"wss://"
} else {
"ws://"
};
let host = url.host_str().ok_or("No host found in URL")?;
let port = url
.port_or_known_default()
.unwrap_or(if url.scheme() == "https" { 443 } else { 80 });
let ws_endpoint = if port == 80 || port == 443 {
format!("{}{}{}", ws_protocol, host, "/ws")
} else {
format!("{}{}:{}/ws", ws_protocol, host, port)
};
Ok(ws_endpoint)
}
#[tauri::command]
pub async fn connect_to_server<R: Runtime>(
tauri_app_handle: AppHandle<R>,
id: String,
client_id: String,
state: tauri::State<'_, WebSocketManager>,
app_handle: AppHandle,
) -> Result<(), String> {
let connections_clone = state.connections.clone();
// Disconnect old connection first
disconnect(client_id.clone(), state.clone()).await.ok();
let server = get_server_by_id(&id)
.await
.ok_or(format!("Server with ID {} not found", id))?;
let endpoint = convert_to_websocket(&server.endpoint)?;
let token = get_server_token(&id).await.map(|t| t.access_token.clone());
let mut request =
tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(&endpoint)
.map_err(|e| format!("Failed to create WebSocket request: {}", e))?;
request
.headers_mut()
.insert("Connection", "Upgrade".parse().unwrap());
request
.headers_mut()
.insert("Upgrade", "websocket".parse().unwrap());
request
.headers_mut()
.insert("Sec-WebSocket-Version", "13".parse().unwrap());
request
.headers_mut()
.insert("Sec-WebSocket-Key", generate_key().parse().unwrap());
if let Some(token) = token {
request
.headers_mut()
.insert("X-API-TOKEN", token.parse().unwrap());
}
let allow_self_signature =
crate::settings::get_allow_self_signature(tauri_app_handle.clone()).await;
let tls_connector = tokio_native_tls::native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(allow_self_signature)
.build()
.map_err(|e| format!("TLS build error: {:?}", e))?;
let connector = Connector::NativeTls(tls_connector.into());
let (ws_stream, _) = connect_async_tls_with_config(
request,
None, // WebSocketConfig
true, // disable_nagle
Some(connector), // Connector
)
.await
.map_err(|e| format!("WebSocket TLS error: {:?}", e))?;
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
let instance = Arc::new(WebSocketInstance {
ws_connection: Mutex::new(ws_stream),
cancel_tx,
});
// Insert connection into the map (lock is held briefly)
{
let mut connections = connections_clone.lock().await;
connections.insert(client_id.clone(), instance.clone());
}
// Spawn WebSocket handler in a separate task
let app_handle_clone = app_handle.clone();
let client_id_clone = client_id.clone();
tokio::spawn(async move {
let ws = &mut *instance.ws_connection.lock().await;
loop {
tokio::select! {
msg = ws.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
let _ = app_handle_clone.emit(&format!("ws-message-{}", client_id_clone), text);
},
Some(Err(_)) | None => {
log::debug!("WebSocket connection closed or error");
let _ = app_handle_clone.emit(&format!("ws-error-{}", client_id_clone), id.clone());
break;
}
_ => {}
}
}
_ = cancel_rx.recv() => {
log::debug!("WebSocket connection cancelled");
let _ = app_handle_clone.emit(&format!("ws-cancel-{}", client_id_clone), id.clone());
break;
}
}
}
// Remove connection after it closes
let mut connections = connections_clone.lock().await;
connections.remove(&client_id_clone);
});
Ok(())
}
#[tauri::command]
pub async fn disconnect(
client_id: String,
state: tauri::State<'_, WebSocketManager>,
) -> Result<(), String> {
let instance = {
let mut connections = state.connections.lock().await;
connections.remove(&client_id)
};
if let Some(instance) = instance {
let _ = instance.cancel_tx.send(()).await;
// Close WebSocket (lock only the connection, not the whole map)
let mut ws = instance.ws_connection.lock().await;
let _ = ws.close(None).await;
}
Ok(())
}

View File

@@ -126,7 +126,6 @@
"https://release.infinilabs.com/coco/app/.latest.json?target={{target}}&arch={{arch}}&current_version={{current_version}}"
]
},
"websocket": {},
"shell": {},
"globalShortcut": {},
"deep-link": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -172,14 +172,6 @@ export function mcp_server_search({
return invokeWithErrorHandler(`mcp_server_search`, { id, queryParams });
}
export function connect_to_server(id: string, clientId: string): Promise<void> {
return invokeWithErrorHandler(`connect_to_server`, { id, clientId });
}
export function disconnect(clientId: string): Promise<void> {
return invokeWithErrorHandler(`disconnect`, { clientId });
}
export function chat_history({
serverId,
from = 0,
@@ -260,25 +252,6 @@ export function cancel_session_chat({
});
}
export function new_chat({
serverId,
websocketId,
message,
queryParams,
}: {
serverId: string;
websocketId: string;
message: string;
queryParams?: Record<string, any>;
}): Promise<GetResponse> {
return invokeWithErrorHandler(`new_chat`, {
serverId,
websocketId,
message,
queryParams,
});
}
export function chat_create({
serverId,
message,
@@ -298,28 +271,6 @@ export function chat_create({
});
}
export function send_message({
serverId,
websocketId,
sessionId,
message,
queryParams,
}: {
serverId: string;
websocketId: string;
sessionId: string;
message: string;
queryParams?: Record<string, any>;
}): Promise<string> {
return invokeWithErrorHandler(`send_message`, {
serverId,
websocketId,
sessionId,
message,
queryParams,
});
}
export function chat_chat({
serverId,
sessionId,

View File

@@ -1,58 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import VisibleKey from "@/components/Common/VisibleKey";
interface ConnectionErrorProps {
reconnect: () => void;
connected: boolean;
}
export default function ConnectionError({
reconnect,
connected,
}: ConnectionErrorProps) {
const { t } = useTranslation();
const [reconnectCountdown, setReconnectCountdown] = useState<number>(0);
useEffect(() => {
if (!reconnectCountdown || connected) {
setReconnectCountdown(0);
return;
}
if (reconnectCountdown > 0) {
const timer = setTimeout(() => {
setReconnectCountdown(reconnectCountdown - 1);
}, 1000);
return () => clearTimeout(timer);
}
}, [reconnectCountdown, connected]);
return (
<div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-[rgba(238,238,238,0.98)] dark:bg-[rgba(32,33,38,0.9)] backdrop-blur-[2px] rounded-md font-normal text-xs text-gray-400 flex items-center gap-4 z-10">
{t("search.input.connectionError")}
<div
className="px-1 h-[24px] text-[#0061FF] font-normal text-xs flex items-center justify-center cursor-pointer underline"
onClick={() => {
reconnect();
setReconnectCountdown(10);
}}
>
{reconnectCountdown > 0 ? (
`${t("search.input.connecting")}(${reconnectCountdown}s)`
) : (
<VisibleKey
shortcut="R"
onKeyPress={() => {
reconnect();
setReconnectCountdown(10);
}}
>
{t("search.input.reconnect")}
</VisibleKey>
)}
</div>
</div>
);
}

View File

@@ -1,56 +0,0 @@
import { useEffect } from "react";
import { Globe } from "lucide-react";
import { useTranslation } from "react-i18next";
import SettingsItem from "./SettingsItem";
import { useAppStore } from "@/stores/appStore";
import { AppEndpoint } from "@/types/index";
const ENDPOINTS = [
{ value: "https://coco.infini.cloud", label: "https://coco.infini.cloud" },
{ value: "http://localhost:9000", label: "http://localhost:9000" },
{ value: "http://infini.tpddns.cn:27200", label: "http://infini.tpddns.cn:27200" },
];
export default function AdvancedSettings() {
const { t } = useTranslation();
const endpoint = useAppStore(state => state.endpoint);
const setEndpoint = useAppStore(state => state.setEndpoint);
useEffect(() => {}, [endpoint]);
const onChangeEndpoint = async (newEndpoint: AppEndpoint) => {
await setEndpoint(newEndpoint);
};
return (
<div className="space-y-8">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t('settings.advanced.title')}
</h2>
<div className="space-y-6">
<SettingsItem
icon={Globe}
title={t('settings.advanced.endpoint.title')}
description={t('settings.advanced.endpoint.description')}
>
<div className={`p-4 rounded-lg`}>
<select
value={endpoint}
onChange={(e) => onChangeEndpoint(e.target.value as AppEndpoint)}
className={`w-full px-3 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white border-gray-300 text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white`}
>
{ENDPOINTS.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</SettingsItem>
</div>
</div>
</div>
);
}

View File

@@ -1,27 +0,0 @@
import { Select } from '@headlessui/react'
interface SettingsSelectProps {
options: string[];
value?: string;
onChange?: (value: string) => void;
}
export default function SettingsSelect({
options,
value,
onChange,
}: SettingsSelectProps) {
return (
<Select
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:ring-blue-500"
>
{options.map((option) => (
<option key={option} value={option.toLowerCase()}>
{option}
</option>
))}
</Select>
);
}

View File

@@ -1,45 +0,0 @@
import { useEffect } from "react";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { LogicalSize } from "@tauri-apps/api/dpi";
// Custom hook to auto-resize window based on content height
const useAutoResizeWindow = () => {
// Function to resize the window to the content's size
const resizeWindowToContent = async () => {
const contentHeight = document.getElementById("main_window")?.scrollHeight || 0;
try {
// Resize the window to fit content size
await getCurrentWebviewWindow()?.setSize(
new LogicalSize(680, contentHeight)
);
console.log("Window resized to content size");
} catch (error) {
console.error("Error resizing window:", error);
}
};
useEffect(() => {
// Initially resize the window
resizeWindowToContent();
// Set up a ResizeObserver to listen for changes in content size
const resizeObserver = new ResizeObserver(() => {
resizeWindowToContent();
});
// Observe the document body for content size changes
resizeObserver.observe(document.body);
// Clean up the observer when the component is unmounted
return () => {
resizeObserver.disconnect();
};
}, []); // Only run once when the component is mounted
// Optionally, you can return values if you need to handle window size elsewhere
};
export default useAutoResizeWindow;

View File

@@ -1,23 +0,0 @@
import { useEffect, RefObject } from 'react';
export function useClickAway(
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}

View File

@@ -1,33 +0,0 @@
import { useState, useEffect } from "react";
import { useConnectStore } from "@/stores/connectStore";
interface UseFeatureControlProps {
initialFeatures: string[];
featureToToggle: string;
condition: (assistant: any) => boolean;
}
export const useFeatureControl = ({
initialFeatures,
featureToToggle,
condition,
}: UseFeatureControlProps) => {
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const [features, setFeatures] = useState<string[]>(initialFeatures);
useEffect(() => {
if (condition(currentAssistant)) {
setFeatures((prev) => prev.filter((feature) => feature !== featureToToggle));
} else {
setFeatures((prev) => {
if (!prev.includes(featureToToggle)) {
return [...prev, featureToToggle];
}
return prev;
});
}
}, [JSON.stringify(currentAssistant), featureToToggle]);
return features;
};

View File

@@ -1,30 +0,0 @@
import { useEffect, useRef } from "react";
const useInfiniteScroll = (callback: () => void) => {
const loaderRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
callback();
}
},
{ threshold: 1.0 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => {
if (loaderRef.current) {
observer.unobserve(loaderRef.current);
}
};
}, [callback]);
return loaderRef;
};
export default useInfiniteScroll;

View File

@@ -1,196 +0,0 @@
import { useEffect, useCallback, useRef } from "react";
import { useWebSocket as useWebSocketAHook } from "ahooks";
import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter";
import { Server } from "@/types/server";
import { useConnectStore } from "@/stores/connectStore";
enum ReadyState {
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
}
interface WebSocketProps {
clientId: string;
connected: boolean;
setConnected: (connected: boolean) => void;
dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>;
onWebsocketSessionId?: (sessionId: string) => void;
}
export default function useWebSocket({
clientId,
connected,
setConnected,
dealMsgRef,
onWebsocketSessionId,
}: WebSocketProps) {
const isTauri = useAppStore((state) => state.isTauri);
const endpoint_websocket = useAppStore((state) => state.endpoint_websocket);
const addError = useAppStore((state) => state.addError);
const currentService = useConnectStore((state) => state.currentService);
const websocketIdRef = useRef<string>("");
const messageQueue = useRef<string[]>([]);
const processingRef = useRef(false);
// web
const { readyState, connect, disconnect } = useWebSocketAHook(
//"wss://coco.infini.cloud/ws",
//"ws://localhost:9000/ws",
isTauri ? "" : endpoint_websocket,
{
manual: true,
reconnectLimit: 3,
reconnectInterval: 3000,
onMessage: (event) => {
const msg = event.data as string;
messageQueue.current.push(msg);
processQueue();
},
}
);
useEffect(() => {
if (!isTauri) {
connect(); // web
}
}, [isTauri, connect]);
const processMessage = useCallback(
(msg: string) => {
try {
if (msg.includes("websocket-session-id")) {
const sessionId = msg.split(":")[1].trim();
websocketIdRef.current = sessionId;
setConnected(true); // web connected
console.log("setConnected:", sessionId);
onWebsocketSessionId?.(sessionId);
} else {
dealMsgRef.current?.(msg);
}
} catch (error) {
console.error("Error processing message:", error, msg);
}
},
[onWebsocketSessionId]
);
const processQueue = useCallback(() => {
if (processingRef.current || messageQueue.current.length === 0) return;
processingRef.current = true;
while (messageQueue.current.length > 0) {
const msg = messageQueue.current.shift();
if (msg) {
// console.log("Processing message:", msg.substring(0, 100));
processMessage(msg);
}
}
processingRef.current = false;
}, [processMessage]);
useEffect(() => {
// web
if (readyState !== ReadyState.Open) {
setConnected(false); // state
}
}, [readyState]);
// Tauri
// 1. WebSocket connects when loading or switching services
// src/components/Assistant/ChatHeader.tsx
// 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket
// src/components/Search/InputBox.tsx
const reconnect = useCallback(
async (server?: Server) => {
setConnected(false); // Disconnect before attempting to reconnect
if (isTauri) {
const targetServer = server || currentService;
if (!targetServer?.id) return;
try {
// console.log("reconnect", targetServer.id);
await platformAdapter.commands("connect_to_server", targetServer.id, clientId);
} catch (error) {
setConnected(false); // error
console.error("Failed to connect:", error);
}
} else {
connect();
}
},
[currentService, clientId]
);
const disconnectWS = useCallback(async () => {
if (!connected) return;
if (isTauri) {
try {
console.log("disconnect");
await platformAdapter.commands("disconnect", clientId);
setConnected(false); // disconnected
} catch (error) {
console.error("Failed to disconnect:", error);
}
} else {
disconnect();
}
}, [connected]);
const updateDealMsg = useCallback(
(newDealMsg: (msg: string) => void) => {
dealMsgRef.current = newDealMsg;
},
[dealMsgRef]
);
const unlistenErrorRef = useRef<Promise<() => void> | null>(null);
const unlistenCancelRef = useRef<Promise<() => void> | null>(null);
useEffect(() => {
if (!isTauri || !currentService?.id) return;
const unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => {
const msg = event.payload as string;
// console.log(`ws-message-${clientId}`, msg);
if (msg.includes("websocket-session-id")) {
const sessionId = msg.split(":")[1].trim();
websocketIdRef.current = sessionId;
console.log("setConnected sessionId:", sessionId);
setConnected(true); // Tauri connected
if (onWebsocketSessionId) {
onWebsocketSessionId(sessionId);
}
// Listen for errors
unlistenErrorRef.current = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => {
if (connected) {
const id = event.payload as string;
console.error(`ws-error-${clientId}`, id === currentService?.id, connected);
if (id === currentService?.id) {
addError("WebSocket connection failed.");
}
}
setConnected(false); // error
});
// Listen for cancel
unlistenCancelRef.current = platformAdapter.listenEvent(`ws-cancel-${clientId}`, () => {
setConnected(false);
});
return;
}
dealMsgRef.current?.(msg);
});
return () => {
unlisten_message.then((fn) => fn());
if (unlistenErrorRef.current) {
unlistenErrorRef.current.then((fn) => fn());
}
if (unlistenCancelRef.current) {
unlistenCancelRef.current.then((fn) => fn());
}
};
}, [currentService?.id, dealMsgRef, connected, clientId]);
return { reconnect, disconnectWS, updateDealMsg };
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function ArrowLeft(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="m7.85 13l2.85 2.85q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L4.7 12.7q-.3-.3-.3-.7t.3-.7l4.575-4.575q.3-.3.713-.287t.712.312q.275.3.288.7t-.288.7L7.85 11H19q.425 0 .713.288T20 12t-.288.713T19 13z"
/>
</SVGWrap>
);
}

View File

@@ -1,16 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function Ask(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13.038 19.927A9.93 9.93 0 0 1 7.7 19L3 20l1.3-3.9C1.976 12.663 2.874 8.228 6.4 5.726c3.526-2.501 8.59-2.296 11.845.48c1.993 1.7 2.93 4.043 2.746 6.346M19 16l-2 3h4l-2 3"
/>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function Link(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 256 256">
<path
fill="currentColor"
d="M117.18 188.74a12 12 0 0 1 0 17l-5.12 5.12A58.26 58.26 0 0 1 70.6 228a58.62 58.62 0 0 1-41.46-100.08l34.75-34.75a58.64 58.64 0 0 1 98.56 28.11a12 12 0 1 1-23.37 5.44a34.65 34.65 0 0 0-58.22-16.58l-34.75 34.75A34.62 34.62 0 0 0 70.57 204a34.41 34.41 0 0 0 24.49-10.14l5.11-5.12a12 12 0 0 1 17.01 0M226.83 45.17a58.65 58.65 0 0 0-82.93 0l-5.11 5.11a12 12 0 0 0 17 17l5.12-5.12a34.63 34.63 0 1 1 49 49l-34.81 34.7A34.39 34.39 0 0 1 150.61 156a34.63 34.63 0 0 1-33.69-26.72a12 12 0 0 0-23.38 5.44A58.64 58.64 0 0 0 150.56 180h.05a58.28 58.28 0 0 0 41.47-17.17l34.75-34.75a58.62 58.62 0 0 0 0-82.91"
/>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function Reload(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V5q0-.425.288-.712T19 4t.713.288T20 5v5q0 .425-.288.713T19 11h-5q-.425 0-.712-.288T13 10t.288-.712T14 9h3.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.7 0 3.113-.862t2.187-2.313q.2-.35.563-.487t.737-.013q.4.125.575.525t-.025.75q-1.025 2-2.925 3.2T12 20"
/>
</SVGWrap>
);
}

View File

@@ -1,15 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function Send(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<g fill="none">
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="m21.433 4.861l-6 15.5a1 1 0 0 1-1.624.362l-3.382-3.235l-2.074 2.073a.5.5 0 0 1-.853-.354v-4.519L2.309 9.723a1 1 0 0 1 .442-1.691l17.5-4.5a1 1 0 0 1 1.181 1.329ZM19 6.001L8.032 13.152l1.735 1.66L19 6Z"
/>
</g>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function Setting(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3.082 13.945c-.529-.95-.793-1.426-.793-1.945c0-.519.264-.994.793-1.944L4.43 7.63l1.426-2.381c.559-.933.838-1.4 1.287-1.66c.45-.259.993-.267 2.08-.285L12 3.26l2.775.044c1.088.018 1.631.026 2.08.286c.45.26.73.726 1.288 1.659L19.57 7.63l1.35 2.426c.528.95.792 1.425.792 1.944c0 .519-.264.994-.793 1.944L19.57 16.37l-1.426 2.381c-.559.933-.838 1.4-1.287 1.66c-.45.259-.993.267-2.08.285L12 20.74l-2.775-.044c-1.088-.018-1.631-.026-2.08-.286c-.45-.26-.73-.726-1.288-1.659L4.43 16.37z" />
<circle cx="12" cy="12" r="3" />
</g>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function ThemeAuto(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-2V4a8 8 0 1 1 0 16"
/>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function ThemeDark(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 21q-3.775 0-6.387-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.325-.05.575.088t.4.362t.163.525t-.188.575q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.275-.175.563-.162t.512.137q.25.125.388.375t.087.6q-.35 3.45-2.937 5.725T12 21m0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19m-.25-6.75"
/>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function ThemeLight(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 15q1.25 0 2.125-.875T15 12t-.875-2.125T12 9t-2.125.875T9 12t.875 2.125T12 15m0 2q-2.075 0-3.537-1.463T7 12t1.463-3.537T12 7t3.538 1.463T17 12t-1.463 3.538T12 17M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm18 0q-.425 0-.712-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8-8q-.425 0-.712-.288T11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5m0 18q-.425 0-.712-.288T11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23M5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7t-.687.288t-.713-.288M18 19.425l-1.05-1.075q-.275-.3-.275-.712t.275-.688q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275M4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288M12 12"
/>
</SVGWrap>
);
}

View File

@@ -1,12 +0,0 @@
import SVGWrap from "./SVGWrap";
export default function UnPin(props: I.SVG) {
return (
<SVGWrap {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M14 4v5c0 1.12.37 2.16 1 3H9c.65-.86 1-1.9 1-3V4zm3-2H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3V4h1c.55 0 1-.45 1-1s-.45-1-1-1"
/>
</SVGWrap>
);
}

View File

@@ -42,11 +42,6 @@
"copyright": "©{{year}} INFINI Labs, All Rights Reserved."
},
"advanced": {
"title": "Advanced Settings",
"endpoint": {
"title": "API Endpoint",
"description": "Domain name for interface and websocket"
},
"startup": {
"title": "Startup Settings",
"defaultStartupWindow": {

View File

@@ -42,11 +42,6 @@
"copyright": "©{{year}} INFINI Labs保留所有权利。"
},
"advanced": {
"title": "高级设置",
"endpoint": {
"title": "API 接口",
"description": "接口和 WebSocket 的域名"
},
"startup": {
"title": "启动设置",
"defaultStartupWindow": {

View File

@@ -1,263 +0,0 @@
// mock
export const res_search = {
took: 2590,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0
},
hits: {
total: {
value: 253,
relation: "eq"
},
max_score: 32.709457,
hits: [
{
_index: "coco_document",
_type: "_doc",
_id: "3ac857ef30d101b1e5880b53b1438b1a",
_score: 32.709457,
_source: {
icon: "web",
id: "3ac857ef30d101b1e5880b53b1438b1a",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Aggregation",
subcategory: "Metric",
title: "Avg aggregation",
content: "",
author: "liaosy",
url: "https://pizza.rs/docs/references/aggregation/avg/",
tags: [
"avg",
"aggregation"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "2485a744c5dae1278a01c04d39bf60a6",
_score: 32.37022,
_source: {
icon: "web",
id: "2485a744c5dae1278a01c04d39bf60a6",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
title: "auto_generate_doc_id",
content: "",
author: "liaosy",
url: "https://infinilabs.cn/docs/latest/gateway/references/filters/auto_generate_doc_id/"
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "15aa340fa9ddfcfbf793b8707a4fa16b",
_score: 21.983166,
_source: {
icon: "web",
id: "15aa340fa9ddfcfbf793b8707a4fa16b",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Overview",
subcategory: "Architecture",
title: "Architecture",
content: "",
author: "yangfan",
url: "https://pizza.rs/docs/overview/architecture/",
tags: [
"architecture"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "f96b1af318d62a43a44f54731409ff52",
_score: 21.887964,
_source: {
icon: "web",
id: "f96b1af318d62a43a44f54731409ff52",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
title: "Introducing Coco AI in Two Minutes - A Quick Start Video 🥥",
content: "",
author: "yangfan",
url: "https://blog.infinilabs.com/posts/2024/a-quick-start-viideo-to-introduce-coco-ai-in-two-minutes/",
tags: [
"Coco AI",
"Search",
"Gen-AI",
"Enterprise"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "3a806937f9e7fe55905a7f71d111e523",
_score: 21.286049,
_source: {
icon: "web",
id: "3a806937f9e7fe55905a7f71d111e523",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Namespace",
title: "Create a namespace",
content: "",
author: "yangfan",
url: "https://pizza.rs/docs/references/namespace/create/",
tags: [
"create",
"namespace"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "04f48643c2c52b872c149e077765f8cb",
_score: 21.286049,
_source: {
icon: "web",
id: "04f48643c2c52b872c149e077765f8cb",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Create a document",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/document/create/",
tags: [
"create",
"index"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "d101818b1e6d2eb23ca2f813ef3a9648",
_score: 21.213268,
_source: {
icon: "web",
id: "d101818b1e6d2eb23ca2f813ef3a9648",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Collection",
title: "Create a collection",
content: " ",
author: "zouwenan",
url: "https://pizza.rs/docs/references/collection/create/",
tags: [
"create",
"collection"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "ee2228755039808c199e6812d09c745e",
_score: 20.967154,
_source: {
icon: "web",
id: "ee2228755039808c199e6812d09c745e",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Delete a document",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/document/delete/",
tags: [
"delete",
"index"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "60584d12aba0ff569e7b79a7be168810",
_score: 20.967154,
_source: {
icon: "web",
id: "60584d12aba0ff569e7b79a7be168810",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Catalog",
subcategory: "Namespace",
title: "Delete a namespace",
content: "",
author: "zouwenan",
url: "https://pizza.rs/docs/references/namespace/delete/",
tags: [
"delete",
"namespace"
]
}
},
{
_index: "coco_document",
_type: "_doc",
_id: "73e31d0feeb8d3d97e4b06a98de54672",
_score: 20.934437,
_source: {
icon: "web",
id: "73e31d0feeb8d3d97e4b06a98de54672",
source: {
name: "hugo_site",
type: "connector"
},
type: "web_page",
category: "Document",
subcategory: "Index",
title: "Replace a document",
content: " ",
author: "medcl",
url: "https://pizza.rs/docs/references/document/replace/",
tags: [
"replace",
"index"
]
}
}
]
}
}

View File

@@ -24,7 +24,6 @@ export type IAppStore = {
endpoint: AppEndpoint;
endpoint_http: string;
endpoint_websocket: string;
setEndpoint: (endpoint: AppEndpoint) => void;
language: string;
setLanguage: (language: string) => void;
@@ -78,20 +77,12 @@ export const useAppStore = create<IAppStore>()(
endpoint: "https://coco.infini.cloud/",
endpoint_http: "https://coco.infini.cloud",
endpoint_websocket: "wss://coco.infini.cloud/ws",
setEndpoint: async (endpoint: AppEndpoint) => {
const endpoint_http = endpoint;
const withoutProtocol = endpoint.split("//")[1];
const endpoint_websocket = endpoint?.includes("https")
? `wss://${withoutProtocol}/ws`
: `ws://${withoutProtocol}/ws`;
return set({
endpoint,
endpoint_http,
endpoint_websocket,
});
},
language: "en",
@@ -128,7 +119,6 @@ export const useAppStore = create<IAppStore>()(
ssoRequestID: state.ssoRequestID,
endpoint: state.endpoint,
endpoint_http: state.endpoint_http,
endpoint_websocket: state.endpoint_websocket,
language: state.language,
}),
}

View File

@@ -13,7 +13,6 @@ export interface EventPayloads {
"endpoint-changed": {
endpoint: string;
endpoint_http: string;
endpoint_websocket: string;
};
"showTooltip-changed": {
showTooltip: boolean;

View File

@@ -1,4 +0,0 @@
export const clientEnv = {
COCO_SERVER_URL: "https://coco.infini.cloud", // http://localhost:9000
COCO_WEBSOCKET_URL: "wss://coco.infini.cloud/ws", // ws://localhost:9000/ws
};

View File

@@ -1,531 +0,0 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
async getRecordingOptions(): Promise<RecordingOptions> {
return await TAURI_INVOKE("get_recording_options");
},
async setRecordingOptions(options: RecordingOptions): Promise<null> {
return await TAURI_INVOKE("set_recording_options", { options });
},
async startRecording(): Promise<null> {
return await TAURI_INVOKE("start_recording");
},
async stopRecording(): Promise<null> {
return await TAURI_INVOKE("stop_recording");
},
async pauseRecording(): Promise<null> {
return await TAURI_INVOKE("pause_recording");
},
async resumeRecording(): Promise<null> {
return await TAURI_INVOKE("resume_recording");
},
async listCameras(): Promise<string[]> {
return await TAURI_INVOKE("list_cameras");
},
async listCaptureWindows(): Promise<CaptureWindow[]> {
return await TAURI_INVOKE("list_capture_windows");
},
async listCaptureScreens(): Promise<CaptureScreen[]> {
return await TAURI_INVOKE("list_capture_screens");
},
async takeScreenshot(): Promise<null> {
return await TAURI_INVOKE("take_screenshot");
},
async listAudioDevices(): Promise<string[]> {
return await TAURI_INVOKE("list_audio_devices");
},
async closePreviousRecordingsWindow(): Promise<void> {
await TAURI_INVOKE("close_previous_recordings_window");
},
async setFakeWindowBounds(name: string, bounds: Bounds): Promise<null> {
return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds });
},
async removeFakeWindow(name: string): Promise<null> {
return await TAURI_INVOKE("remove_fake_window", { name });
},
async focusCapturesPanel(): Promise<void> {
await TAURI_INVOKE("focus_captures_panel");
},
async getCurrentRecording(): Promise<JsonValue<RecordingInfo | null>> {
return await TAURI_INVOKE("get_current_recording");
},
async exportVideo(
videoId: string,
project: ProjectConfiguration,
progress: TAURI_CHANNEL<RenderProgress>,
force: boolean
): Promise<string> {
return await TAURI_INVOKE("export_video", {
videoId,
project,
progress,
force,
});
},
async copyFileToPath(src: string, dst: string): Promise<null> {
return await TAURI_INVOKE("copy_file_to_path", { src, dst });
},
async copyVideoToClipboard(path: string): Promise<null> {
return await TAURI_INVOKE("copy_video_to_clipboard", { path });
},
async copyScreenshotToClipboard(path: string): Promise<null> {
return await TAURI_INVOKE("copy_screenshot_to_clipboard", { path });
},
async openFilePath(path: string): Promise<null> {
return await TAURI_INVOKE("open_file_path", { path });
},
async getVideoMetadata(
videoId: string,
videoType: VideoType | null
): Promise<VideoRecordingMetadata> {
return await TAURI_INVOKE("get_video_metadata", { videoId, videoType });
},
async createEditorInstance(
videoId: string
): Promise<SerializedEditorInstance> {
return await TAURI_INVOKE("create_editor_instance", { videoId });
},
async startPlayback(videoId: string): Promise<void> {
await TAURI_INVOKE("start_playback", { videoId });
},
async stopPlayback(videoId: string): Promise<void> {
await TAURI_INVOKE("stop_playback", { videoId });
},
async setPlayheadPosition(
videoId: string,
frameNumber: number
): Promise<void> {
await TAURI_INVOKE("set_playhead_position", { videoId, frameNumber });
},
async setProjectConfig(
videoId: string,
config: ProjectConfiguration
): Promise<void> {
await TAURI_INVOKE("set_project_config", { videoId, config });
},
async openEditor(id: string): Promise<void> {
await TAURI_INVOKE("open_editor", { id });
},
async openMainWindow(): Promise<void> {
await TAURI_INVOKE("open_main_window");
},
async openPermissionSettings(permission: OSPermission): Promise<void> {
await TAURI_INVOKE("open_permission_settings", { permission });
},
async doPermissionsCheck(initialCheck: boolean): Promise<OSPermissionsCheck> {
return await TAURI_INVOKE("do_permissions_check", { initialCheck });
},
async requestPermission(permission: OSPermission): Promise<void> {
await TAURI_INVOKE("request_permission", { permission });
},
async uploadExportedVideo(
videoId: string,
mode: UploadMode
): Promise<UploadResult> {
return await TAURI_INVOKE("upload_exported_video", { videoId, mode });
},
async uploadScreenshot(screenshotPath: string): Promise<UploadResult> {
return await TAURI_INVOKE("upload_screenshot", { screenshotPath });
},
async getRecordingMeta(id: string, fileType: string): Promise<RecordingMeta> {
return await TAURI_INVOKE("get_recording_meta", { id, fileType });
},
async saveFileDialog(
fileName: string,
fileType: string
): Promise<string | null> {
return await TAURI_INVOKE("save_file_dialog", { fileName, fileType });
},
async listRecordings(): Promise<[string, string, RecordingMeta][]> {
return await TAURI_INVOKE("list_recordings");
},
async listScreenshots(): Promise<[string, string, RecordingMeta][]> {
return await TAURI_INVOKE("list_screenshots");
},
async checkUpgradedAndUpdate(): Promise<boolean> {
return await TAURI_INVOKE("check_upgraded_and_update");
},
async openExternalLink(url: string): Promise<null> {
return await TAURI_INVOKE("open_external_link", { url });
},
async setHotkey(action: HotkeyAction, hotkey: Hotkey | null): Promise<null> {
return await TAURI_INVOKE("set_hotkey", { action, hotkey });
},
async deleteAuthOpenSignin(): Promise<null> {
return await TAURI_INVOKE("delete_auth_open_signin");
},
async resetCameraPermissions(): Promise<null> {
return await TAURI_INVOKE("reset_camera_permissions");
},
async resetMicrophonePermissions(): Promise<null> {
return await TAURI_INVOKE("reset_microphone_permissions");
},
async isCameraWindowOpen(): Promise<boolean> {
return await TAURI_INVOKE("is_camera_window_open");
},
async seekTo(videoId: string, frameNumber: number): Promise<void> {
await TAURI_INVOKE("seek_to", { videoId, frameNumber });
},
async sendFeedbackRequest(feedback: string): Promise<null> {
return await TAURI_INVOKE("send_feedback_request", { feedback });
},
async positionTrafficLights(
controlsInset: [number, number] | null
): Promise<void> {
await TAURI_INVOKE("position_traffic_lights", { controlsInset });
},
async setTheme(theme: AppTheme): Promise<void> {
await TAURI_INVOKE("set_theme", { theme });
},
async globalMessageDialog(message: string): Promise<void> {
await TAURI_INVOKE("global_message_dialog", { message });
},
async showWindow(window: ShowCapWindow): Promise<void> {
await TAURI_INVOKE("show_window", { window });
},
async writeClipboardString(text: string): Promise<null> {
return await TAURI_INVOKE("write_clipboard_string", { text });
},
};
/** user-defined events **/
export const events = __makeEvents__<{
audioInputLevelChange: AudioInputLevelChange;
authenticationInvalid: AuthenticationInvalid;
currentRecordingChanged: CurrentRecordingChanged;
editorStateChanged: EditorStateChanged;
newNotification: NewNotification;
newRecordingAdded: NewRecordingAdded;
newScreenshotAdded: NewScreenshotAdded;
recordingMetaChanged: RecordingMetaChanged;
recordingOptionsChanged: RecordingOptionsChanged;
recordingStarted: RecordingStarted;
recordingStopped: RecordingStopped;
renderFrameEvent: RenderFrameEvent;
requestNewScreenshot: RequestNewScreenshot;
requestOpenSettings: RequestOpenSettings;
requestRestartRecording: RequestRestartRecording;
requestStartRecording: RequestStartRecording;
requestStopRecording: RequestStopRecording;
uploadProgress: UploadProgress;
}>({
audioInputLevelChange: "audio-input-level-change",
authenticationInvalid: "authentication-invalid",
currentRecordingChanged: "current-recording-changed",
editorStateChanged: "editor-state-changed",
newNotification: "new-notification",
newRecordingAdded: "new-recording-added",
newScreenshotAdded: "new-screenshot-added",
recordingMetaChanged: "recording-meta-changed",
recordingOptionsChanged: "recording-options-changed",
recordingStarted: "recording-started",
recordingStopped: "recording-stopped",
renderFrameEvent: "render-frame-event",
requestNewScreenshot: "request-new-screenshot",
requestOpenSettings: "request-open-settings",
requestRestartRecording: "request-restart-recording",
requestStartRecording: "request-start-recording",
requestStopRecording: "request-stop-recording",
uploadProgress: "upload-progress",
});
/** user-defined constants **/
/** user-defined types **/
export type AppEndpoint = string;
export type AppTheme = "auto" | "light" | "dark";
export type WindowTheme = "light" | "dark";
export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall";
export type Audio = { duration: number; sample_rate: number; channels: number };
export type AudioConfiguration = { mute: boolean; improve: boolean };
export type AudioInputLevelChange = number;
export type AudioMeta = { path: string };
export type AuthStore = {
token: string;
user_id: string | null;
expires: number;
plan: Plan | null;
};
export type AuthenticationInvalid = null;
export type BackgroundConfiguration = {
source: BackgroundSource;
blur: number;
padding: number;
rounding: number;
inset: number;
crop: Crop | null;
};
export type BackgroundSource =
| { type: "wallpaper"; id: number }
| { type: "image"; path: string | null }
| { type: "color"; value: [number, number, number] }
| {
type: "gradient";
from: [number, number, number];
to: [number, number, number];
angle?: number;
};
export type Bounds = { x: number; y: number; width: number; height: number };
export type Camera = {
hide: boolean;
mirror: boolean;
position: CameraPosition;
size: number;
zoom_size: number | null;
rounding: number;
shadow: number;
};
export type CameraMeta = { path: string };
export type CameraPosition = { x: CameraXPosition; y: CameraYPosition };
export type CameraXPosition = "left" | "center" | "right";
export type CameraYPosition = "top" | "bottom";
export type CaptureScreen = { id: number; name: string };
export type CaptureWindow = {
id: number;
owner_name: string;
name: string;
bounds: Bounds;
};
export type Crop = { position: XY<number>; size: XY<number> };
export type CurrentRecordingChanged = null;
export type CursorAnimationStyle = "regular" | "slow" | "fast";
export type CursorConfiguration = {
hideWhenIdle: boolean;
size: number;
type: CursorType;
animationStyle: CursorAnimationStyle;
};
export type CursorType = "pointer" | "circle";
export type Display = { path: string };
export type EditorStateChanged = { playhead_position: number };
export type Flags = {
recordMouse: boolean;
split: boolean;
pauseResume: boolean;
zoom: boolean;
customS3: boolean;
};
export type GeneralSettingsStore = {
uploadIndividualFiles?: boolean;
openEditorAfterRecording?: boolean;
hideDockIcon?: boolean;
autoCreateShareableLink?: boolean;
enableNotifications?: boolean;
disableAutoOpenLinks?: boolean;
hasCompletedStartup?: boolean;
theme?: AppTheme;
};
export type Hotkey = {
code: string;
meta: boolean;
ctrl: boolean;
alt: boolean;
shift: boolean;
};
export type HotkeyAction =
| "startRecording"
| "stopRecording"
| "restartRecording"
| "takeScreenshot";
export type HotkeysConfiguration = { show: boolean };
export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } };
export type JsonValue<T> = [T];
export type MultipleSegment = {
display: Display;
camera?: CameraMeta | null;
audio?: AudioMeta | null;
cursor?: string | null;
};
export type MultipleSegments = {
segments: MultipleSegment[];
cursors: { [key in string]: string };
};
export type NewNotification = {
title: string;
body: string;
is_error: boolean;
};
export type NewRecordingAdded = { path: string };
export type NewScreenshotAdded = { path: string };
export type OSPermission =
| "screenRecording"
| "camera"
| "microphone"
| "accessibility";
export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied";
export type OSPermissionsCheck = {
screenRecording: OSPermissionStatus;
microphone: OSPermissionStatus;
camera: OSPermissionStatus;
accessibility: OSPermissionStatus;
};
export type Plan = { upgraded: boolean; last_checked: number };
export type PreCreatedVideo = {
id: string;
link: string;
config: S3UploadMeta;
};
export type ProjectConfiguration = {
aspectRatio: AspectRatio | null;
background: BackgroundConfiguration;
camera: Camera;
audio: AudioConfiguration;
cursor: CursorConfiguration;
hotkeys: HotkeysConfiguration;
timeline?: TimelineConfiguration | null;
motionBlur: number | null;
};
export type ProjectRecordings = { segments: SegmentRecordings[] };
export type RecordingInfo = { captureTarget: ScreenCaptureTarget };
export type RecordingMeta = (
| { segment: SingleSegment }
| { inner: MultipleSegments }
) & { pretty_name: string; sharing?: SharingMeta | null };
export type RecordingMetaChanged = { id: string };
export type RecordingOptions = {
captureTarget: ScreenCaptureTarget;
cameraLabel: string | null;
audioInputName: string | null;
};
export type RecordingOptionsChanged = null;
export type RecordingStarted = null;
export type RecordingStopped = { path: string };
export type RenderFrameEvent = { frame_number: number };
export type RenderProgress =
| { type: "Starting"; total_frames: number }
| { type: "EstimatedTotalFrames"; total_frames: number }
| { type: "FrameRendered"; current_frame: number };
export type RequestNewScreenshot = null;
export type RequestOpenSettings = { page: string };
export type RequestRestartRecording = null;
export type RequestStartRecording = null;
export type RequestStopRecording = null;
export type S3UploadMeta = {
id: string;
user_id: string;
aws_region?: string;
aws_bucket?: string;
};
export type ScreenCaptureTarget =
| ({ variant: "window" } & CaptureWindow)
| ({ variant: "screen" } & CaptureScreen);
export type SegmentRecordings = {
display: Video;
camera: Video | null;
audio: Audio | null;
};
export type SerializedEditorInstance = {
framesSocketUrl: string;
recordingDuration: number;
savedProjectConfig: ProjectConfiguration;
recordings: ProjectRecordings;
path: string;
prettyName: string;
};
export type SharingMeta = { id: string; link: string };
export type ShowCapWindow =
| "Setup"
| "Main"
| { Settings: { page: string | null } }
| { Editor: { project_id: string } }
| "PrevRecordings"
| "WindowCaptureOccluder"
| { Camera: { ws_port: number } }
| { InProgressRecording: { position: [number, number] | null } }
| "Upgrade";
export type SingleSegment = {
display: Display;
camera?: CameraMeta | null;
audio?: AudioMeta | null;
cursor?: string | null;
};
export type TimelineConfiguration = {
segments: TimelineSegment[];
zoomSegments?: ZoomSegment[];
};
export type TimelineSegment = {
recordingSegment: number | null;
timescale: number;
start: number;
end: number;
};
export type UploadMode =
| { Initial: { pre_created_video: PreCreatedVideo | null } }
| "Reupload";
export type UploadProgress = {
stage: string;
progress: number;
message: string;
};
export type UploadResult =
| { Success: string }
| "NotAuthenticated"
| "PlanCheckFailed"
| "UpgradeRequired";
export type Video = { duration: number; width: number; height: number };
export type VideoRecordingMetadata = { duration: number; size: number };
export type VideoType = "screen" | "output";
export type XY<T> = { x: T; y: T };
export type ZoomSegment = { start: number; end: number; amount: number };
/** tauri-specta globals **/
import {
invoke as TAURI_INVOKE,
Channel as TAURI_CHANNEL,
} from "@tauri-apps/api/core";
import * as TAURI_API_EVENT from "@tauri-apps/api/event";
import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
type __EventObj__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: null extends T
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case "listen":
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case "once":
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case "emit":
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
}
);
}

1
src/vite-env.d.ts vendored
View File

@@ -2,7 +2,6 @@
interface ImportMetaEnv {
readonly COCO_SERVER_URL: string;
readonly COCO_WEBSOCKET_URL: string;
// more env variables...
}