diff --git a/.env.template b/.env.template index 82f44216ab8..96dedddc8aa 100644 --- a/.env.template +++ b/.env.template @@ -1,12 +1,20 @@ # Your openai api key. (required) OPENAI_API_KEY=sk-xxxx +# DeepSeek Api Key. (Optional) +DEEPSEEK_API_KEY= + # Access password, separated by comma. (optional) CODE=your-password # You can start service behind a proxy. (optional) PROXY_URL=http://localhost:7890 +# Enable MCP functionality (optional) +# Default: Empty (disabled) +# Set to "true" to enable MCP functionality +ENABLE_MCP= + # (optional) # Default: Empty # Google Gemini Pro API key, set if you want to use Google Gemini Pro API. @@ -66,4 +74,16 @@ ANTHROPIC_API_VERSION= ANTHROPIC_URL= ### (optional) -WHITE_WEBDAV_ENDPOINTS= \ No newline at end of file +WHITE_WEBDAV_ENDPOINTS= + +### siliconflow Api key (optional) +SILICONFLOW_API_KEY= + +### siliconflow Api url (optional) +SILICONFLOW_URL= + +### 302.AI Api key (optional) +AI302_API_KEY= + +### 302.AI Api url (optional) +AI302_URL= diff --git a/.eslintignore b/.eslintignore index 08975255475..61e76e59ae5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,3 @@ -public/serviceWorker.js \ No newline at end of file +public/serviceWorker.js +app/mcp/mcp_config.json +app/mcp/mcp_config.default.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2ff556f646e..b1c2bfefad3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ dev *.key.pub masks.json + +# mcp config +app/mcp/mcp_config.json diff --git a/Dockerfile b/Dockerfile index ae9a17cddbd..d3e4193eef2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,16 @@ ENV PROXY_URL="" ENV OPENAI_API_KEY="" ENV GOOGLE_API_KEY="" ENV CODE="" +ENV ENABLE_MCP="" COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/server ./.next/server +RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp +COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json + EXPOSE 3000 CMD if [ -n "$PROXY_URL" ]; then \ diff --git a/LICENSE b/LICENSE index 047f9431e7d..4864ab00d2c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 Zhang Yifei +Copyright (c) 2023-2025 NextChat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d370000fa68..2e45c7120fa 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@
)/g,
diff --git a/app/components/mask.tsx b/app/components/mask.tsx
index 12b19e33514..fa9537cbafa 100644
--- a/app/components/mask.tsx
+++ b/app/components/mask.tsx
@@ -55,6 +55,7 @@ import {
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils";
+import clsx from "clsx";
// drag and drop helper function
function reorder(list: T[], startIndex: number, endIndex: number): T[] {
@@ -588,7 +589,7 @@ export function MaskPage() {
diff --git a/app/components/realtime-chat/index.ts b/app/components/realtime-chat/index.ts
new file mode 100644
index 00000000000..fdf090f4195
--- /dev/null
+++ b/app/components/realtime-chat/index.ts
@@ -0,0 +1 @@
+export * from "./realtime-chat";
diff --git a/app/components/realtime-chat/realtime-chat.module.scss b/app/components/realtime-chat/realtime-chat.module.scss
new file mode 100644
index 00000000000..ef58bebb655
--- /dev/null
+++ b/app/components/realtime-chat/realtime-chat.module.scss
@@ -0,0 +1,74 @@
+.realtime-chat {
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ padding: 20px;
+ box-sizing: border-box;
+ .circle-mic {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ .icon-center {
+ font-size: 24px;
+ }
+
+ .bottom-icons {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ position: absolute;
+ bottom: 20px;
+ box-sizing: border-box;
+ padding: 0 20px;
+ }
+
+ .icon-left,
+ .icon-right {
+ width: 46px;
+ height: 46px;
+ font-size: 36px;
+ background: var(--second);
+ border-radius: 50%;
+ padding: 2px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ &.mobile {
+ display: none;
+ }
+}
+
+.pulse {
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 0.7;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 0.7;
+ }
+}
diff --git a/app/components/realtime-chat/realtime-chat.tsx b/app/components/realtime-chat/realtime-chat.tsx
new file mode 100644
index 00000000000..faa36373a2c
--- /dev/null
+++ b/app/components/realtime-chat/realtime-chat.tsx
@@ -0,0 +1,359 @@
+import VoiceIcon from "@/app/icons/voice.svg";
+import VoiceOffIcon from "@/app/icons/voice-off.svg";
+import PowerIcon from "@/app/icons/power.svg";
+
+import styles from "./realtime-chat.module.scss";
+import clsx from "clsx";
+
+import { useState, useRef, useEffect } from "react";
+
+import { useChatStore, createMessage, useAppConfig } from "@/app/store";
+
+import { IconButton } from "@/app/components/button";
+
+import {
+ Modality,
+ RTClient,
+ RTInputAudioItem,
+ RTResponse,
+ TurnDetection,
+} from "rt-client";
+import { AudioHandler } from "@/app/lib/audio";
+import { uploadImage } from "@/app/utils/chat";
+import { VoicePrint } from "@/app/components/voice-print";
+
+interface RealtimeChatProps {
+ onClose?: () => void;
+ onStartVoice?: () => void;
+ onPausedVoice?: () => void;
+}
+
+export function RealtimeChat({
+ onClose,
+ onStartVoice,
+ onPausedVoice,
+}: RealtimeChatProps) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const config = useAppConfig();
+ const [status, setStatus] = useState("");
+ const [isRecording, setIsRecording] = useState(false);
+ const [isConnected, setIsConnected] = useState(false);
+ const [isConnecting, setIsConnecting] = useState(false);
+ const [modality, setModality] = useState("audio");
+ const [useVAD, setUseVAD] = useState(true);
+ const [frequencies, setFrequencies] = useState();
+
+ const clientRef = useRef(null);
+ const audioHandlerRef = useRef(null);
+ const initRef = useRef(false);
+
+ const temperature = config.realtimeConfig.temperature;
+ const apiKey = config.realtimeConfig.apiKey;
+ const model = config.realtimeConfig.model;
+ const azure = config.realtimeConfig.provider === "Azure";
+ const azureEndpoint = config.realtimeConfig.azure.endpoint;
+ const azureDeployment = config.realtimeConfig.azure.deployment;
+ const voice = config.realtimeConfig.voice;
+
+ const handleConnect = async () => {
+ if (isConnecting) return;
+ if (!isConnected) {
+ try {
+ setIsConnecting(true);
+ clientRef.current = azure
+ ? new RTClient(
+ new URL(azureEndpoint),
+ { key: apiKey },
+ { deployment: azureDeployment },
+ )
+ : new RTClient({ key: apiKey }, { model });
+ const modalities: Modality[] =
+ modality === "audio" ? ["text", "audio"] : ["text"];
+ const turnDetection: TurnDetection = useVAD
+ ? { type: "server_vad" }
+ : null;
+ await clientRef.current.configure({
+ instructions: "",
+ voice,
+ input_audio_transcription: { model: "whisper-1" },
+ turn_detection: turnDetection,
+ tools: [],
+ temperature,
+ modalities,
+ });
+ startResponseListener();
+
+ setIsConnected(true);
+ // TODO
+ // try {
+ // const recentMessages = chatStore.getMessagesWithMemory();
+ // for (const message of recentMessages) {
+ // const { role, content } = message;
+ // if (typeof content === "string") {
+ // await clientRef.current.sendItem({
+ // type: "message",
+ // role: role as any,
+ // content: [
+ // {
+ // type: (role === "assistant" ? "text" : "input_text") as any,
+ // text: content as string,
+ // },
+ // ],
+ // });
+ // }
+ // }
+ // // await clientRef.current.generateResponse();
+ // } catch (error) {
+ // console.error("Set message failed:", error);
+ // }
+ } catch (error) {
+ console.error("Connection failed:", error);
+ setStatus("Connection failed");
+ } finally {
+ setIsConnecting(false);
+ }
+ } else {
+ await disconnect();
+ }
+ };
+
+ const disconnect = async () => {
+ if (clientRef.current) {
+ try {
+ await clientRef.current.close();
+ clientRef.current = null;
+ setIsConnected(false);
+ } catch (error) {
+ console.error("Disconnect failed:", error);
+ }
+ }
+ };
+
+ const startResponseListener = async () => {
+ if (!clientRef.current) return;
+
+ try {
+ for await (const serverEvent of clientRef.current.events()) {
+ if (serverEvent.type === "response") {
+ await handleResponse(serverEvent);
+ } else if (serverEvent.type === "input_audio") {
+ await handleInputAudio(serverEvent);
+ }
+ }
+ } catch (error) {
+ if (clientRef.current) {
+ console.error("Response iteration error:", error);
+ }
+ }
+ };
+
+ const handleResponse = async (response: RTResponse) => {
+ for await (const item of response) {
+ if (item.type === "message" && item.role === "assistant") {
+ const botMessage = createMessage({
+ role: item.role,
+ content: "",
+ });
+ // add bot message first
+ chatStore.updateTargetSession(session, (session) => {
+ session.messages = session.messages.concat([botMessage]);
+ });
+ let hasAudio = false;
+ for await (const content of item) {
+ if (content.type === "text") {
+ for await (const text of content.textChunks()) {
+ botMessage.content += text;
+ }
+ } else if (content.type === "audio") {
+ const textTask = async () => {
+ for await (const text of content.transcriptChunks()) {
+ botMessage.content += text;
+ }
+ };
+ const audioTask = async () => {
+ audioHandlerRef.current?.startStreamingPlayback();
+ for await (const audio of content.audioChunks()) {
+ hasAudio = true;
+ audioHandlerRef.current?.playChunk(audio);
+ }
+ };
+ await Promise.all([textTask(), audioTask()]);
+ }
+ // update message.content
+ chatStore.updateTargetSession(session, (session) => {
+ session.messages = session.messages.concat();
+ });
+ }
+ if (hasAudio) {
+ // upload audio get audio_url
+ const blob = audioHandlerRef.current?.savePlayFile();
+ uploadImage(blob!).then((audio_url) => {
+ botMessage.audio_url = audio_url;
+ // update text and audio_url
+ chatStore.updateTargetSession(session, (session) => {
+ session.messages = session.messages.concat();
+ });
+ });
+ }
+ }
+ }
+ };
+
+ const handleInputAudio = async (item: RTInputAudioItem) => {
+ await item.waitForCompletion();
+ if (item.transcription) {
+ const userMessage = createMessage({
+ role: "user",
+ content: item.transcription,
+ });
+ chatStore.updateTargetSession(session, (session) => {
+ session.messages = session.messages.concat([userMessage]);
+ });
+ // save input audio_url, and update session
+ const { audioStartMillis, audioEndMillis } = item;
+ // upload audio get audio_url
+ const blob = audioHandlerRef.current?.saveRecordFile(
+ audioStartMillis,
+ audioEndMillis,
+ );
+ uploadImage(blob!).then((audio_url) => {
+ userMessage.audio_url = audio_url;
+ chatStore.updateTargetSession(session, (session) => {
+ session.messages = session.messages.concat();
+ });
+ });
+ }
+ // stop streaming play after get input audio.
+ audioHandlerRef.current?.stopStreamingPlayback();
+ };
+
+ const toggleRecording = async () => {
+ if (!isRecording && clientRef.current) {
+ try {
+ if (!audioHandlerRef.current) {
+ audioHandlerRef.current = new AudioHandler();
+ await audioHandlerRef.current.initialize();
+ }
+ await audioHandlerRef.current.startRecording(async (chunk) => {
+ await clientRef.current?.sendAudio(chunk);
+ });
+ setIsRecording(true);
+ } catch (error) {
+ console.error("Failed to start recording:", error);
+ }
+ } else if (audioHandlerRef.current) {
+ try {
+ audioHandlerRef.current.stopRecording();
+ if (!useVAD) {
+ const inputAudio = await clientRef.current?.commitAudio();
+ await handleInputAudio(inputAudio!);
+ await clientRef.current?.generateResponse();
+ }
+ setIsRecording(false);
+ } catch (error) {
+ console.error("Failed to stop recording:", error);
+ }
+ }
+ };
+
+ useEffect(() => {
+ // 防止重复初始化
+ if (initRef.current) return;
+ initRef.current = true;
+
+ const initAudioHandler = async () => {
+ const handler = new AudioHandler();
+ await handler.initialize();
+ audioHandlerRef.current = handler;
+ await handleConnect();
+ await toggleRecording();
+ };
+
+ initAudioHandler().catch((error) => {
+ setStatus(error);
+ console.error(error);
+ });
+
+ return () => {
+ if (isRecording) {
+ toggleRecording();
+ }
+ audioHandlerRef.current?.close().catch(console.error);
+ disconnect();
+ };
+ }, []);
+
+ useEffect(() => {
+ let animationFrameId: number;
+
+ if (isConnected && isRecording) {
+ const animationFrame = () => {
+ if (audioHandlerRef.current) {
+ const freqData = audioHandlerRef.current.getByteFrequencyData();
+ setFrequencies(freqData);
+ }
+ animationFrameId = requestAnimationFrame(animationFrame);
+ };
+
+ animationFrameId = requestAnimationFrame(animationFrame);
+ } else {
+ setFrequencies(undefined);
+ }
+
+ return () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ };
+ }, [isConnected, isRecording]);
+
+ // update session params
+ useEffect(() => {
+ clientRef.current?.configure({ voice });
+ }, [voice]);
+ useEffect(() => {
+ clientRef.current?.configure({ temperature });
+ }, [temperature]);
+
+ const handleClose = async () => {
+ onClose?.();
+ if (isRecording) {
+ await toggleRecording();
+ }
+ disconnect().catch(console.error);
+ };
+
+ return (
+
+
+
+
+
+
+
+ : }
+ onClick={toggleRecording}
+ disabled={!isConnected}
+ shadow
+ bordered
+ />
+
+ {status}
+
+ }
+ onClick={handleClose}
+ shadow
+ bordered
+ />
+
+
+
+ );
+}
diff --git a/app/components/realtime-chat/realtime-config.tsx b/app/components/realtime-chat/realtime-config.tsx
new file mode 100644
index 00000000000..08809afda2f
--- /dev/null
+++ b/app/components/realtime-chat/realtime-config.tsx
@@ -0,0 +1,173 @@
+import { RealtimeConfig } from "@/app/store";
+
+import Locale from "@/app/locales";
+import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
+
+import { InputRange } from "@/app/components/input-range";
+import { Voice } from "rt-client";
+import { ServiceProvider } from "@/app/constant";
+
+const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
+
+const models = ["gpt-4o-realtime-preview-2024-10-01"];
+
+const voice = ["alloy", "shimmer", "echo"];
+
+export function RealtimeConfigList(props: {
+ realtimeConfig: RealtimeConfig;
+ updateConfig: (updater: (config: RealtimeConfig) => void) => void;
+}) {
+ const azureConfigComponent = props.realtimeConfig.provider ===
+ ServiceProvider.Azure && (
+ <>
+
+ {
+ props.updateConfig(
+ (config) => (config.azure.endpoint = e.currentTarget.value),
+ );
+ }}
+ />
+
+
+ {
+ props.updateConfig(
+ (config) => (config.azure.deployment = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+
+ return (
+ <>
+
+
+ props.updateConfig(
+ (config) => (config.enable = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+ {props.realtimeConfig.enable && (
+ <>
+
+
+
+
+
+
+
+ {
+ props.updateConfig(
+ (config) => (config.apiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ {azureConfigComponent}
+
+
+
+
+ {
+ props.updateConfig(
+ (config) =>
+ (config.temperature = e.currentTarget.valueAsNumber),
+ );
+ }}
+ >
+
+ >
+ )}
+ >
+ );
+}
diff --git a/app/components/sd/sd-panel.tsx b/app/components/sd/sd-panel.tsx
index a71e560ddef..15aff0ab608 100644
--- a/app/components/sd/sd-panel.tsx
+++ b/app/components/sd/sd-panel.tsx
@@ -4,6 +4,7 @@ import { Select } from "@/app/components/ui-lib";
import { IconButton } from "@/app/components/button";
import Locale from "@/app/locales";
import { useSdStore } from "@/app/store/sd";
+import clsx from "clsx";
export const params = [
{
@@ -136,7 +137,7 @@ export function ControlParamItem(props: {
className?: string;
}) {
return (
-
+
diff --git a/app/components/sd/sd.tsx b/app/components/sd/sd.tsx
index 0ace62a83cb..1ccc0647e4c 100644
--- a/app/components/sd/sd.tsx
+++ b/app/components/sd/sd.tsx
@@ -36,6 +36,7 @@ import { removeImage } from "@/app/utils/chat";
import { SideBar } from "./sd-sidebar";
import { WindowContent } from "@/app/components/home";
import { params } from "./sd-panel";
+import clsx from "clsx";
function getSdTaskStatus(item: any) {
let s: string;
@@ -104,7 +105,7 @@ export function Sd() {
return (
<>
-
+
@@ -121,7 +122,10 @@ export function Sd() {
)}
Stability AI
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index 666caece838..881c12caeb3 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -72,6 +72,10 @@ import {
Stability,
Iflytek,
SAAS_CHAT_URL,
+ ChatGLM,
+ DeepSeek,
+ SiliconFlow,
+ AI302,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
@@ -84,6 +88,7 @@ import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
import { TTSConfigList } from "./tts-config";
+import { RealtimeConfigList } from "./realtime-chat/realtime-config";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
@@ -1195,6 +1200,47 @@ export function Settings() {
>
);
+ const deepseekConfigComponent = accessStore.provider ===
+ ServiceProvider.DeepSeek && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.deepseekUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.deepseekApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
<>
);
+ const chatglmConfigComponent = accessStore.provider ===
+ ServiceProvider.ChatGLM && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.chatglmUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.chatglmApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+ const siliconflowConfigComponent = accessStore.provider ===
+ ServiceProvider.SiliconFlow && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.siliconflowUrl = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.siliconflowApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+
const stabilityConfigComponent = accessStore.provider ===
ServiceProvider.Stability && (
<>
@@ -1332,6 +1459,46 @@ export function Settings() {
>
);
+ const ai302ConfigComponent = accessStore.provider === ServiceProvider["302.AI"] && (
+ <>
+
+
+ accessStore.update(
+ (access) => (access.ai302Url = e.currentTarget.value),
+ )
+ }
+ >
+
+
+ {
+ accessStore.update(
+ (access) => (access.ai302ApiKey = e.currentTarget.value),
+ );
+ }}
+ />
+
+ >
+ );
+
return (
@@ -1690,9 +1857,13 @@ export function Settings() {
{alibabaConfigComponent}
{tencentConfigComponent}
{moonshotConfigComponent}
+ {deepseekConfigComponent}
{stabilityConfigComponent}
{lflytekConfigComponent}
{XAIConfigComponent}
+ {chatglmConfigComponent}
+ {siliconflowConfigComponent}
+ {ai302ConfigComponent}
>
)}
>
@@ -1727,9 +1898,11 @@ export function Settings() {
setShowPromptModal(false)} />
)}
-
+
+ {
+ const realtimeConfig = { ...config.realtimeConfig };
+ updater(realtimeConfig);
+ config.update(
+ (config) => (config.realtimeConfig = realtimeConfig),
+ );
+ }}
+ />
+
(await import("./chat-list")).ChatList, {
loading: () => null,
@@ -127,6 +135,7 @@ export function useDragSideBar() {
shouldNarrow,
};
}
+
export function SideBarContainer(props: {
children: React.ReactNode;
onDragStart: (e: MouseEvent) => void;
@@ -141,9 +150,9 @@ export function SideBarContainer(props: {
const { children, className, onDragStart, shouldNarrow } = props;
return (
@@ -182,7 +191,7 @@ export function SideBarHeader(props: {
{subTitle}
- {logo}
+ {logo}
{children}
@@ -218,10 +227,21 @@ export function SideBarTail(props: {
export function SideBar(props: { className?: string }) {
useHotKey();
const { onDragStart, shouldNarrow } = useDragSideBar();
- const [showPluginSelector, setShowPluginSelector] = useState(false);
+ const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
const navigate = useNavigate();
const config = useAppConfig();
const chatStore = useChatStore();
+ const [mcpEnabled, setMcpEnabled] = useState(false);
+
+ useEffect(() => {
+ // 检查 MCP 是否启用
+ const checkMcpStatus = async () => {
+ const enabled = await isMcpEnabled();
+ setMcpEnabled(enabled);
+ console.log("[SideBar] MCP enabled:", enabled);
+ };
+ checkMcpStatus();
+ }, []);
return (
+ {mcpEnabled && (
+ }
+ text={shouldNarrow ? undefined : Locale.Mcp.Name}
+ className={styles["sidebar-bar-button"]}
+ onClick={() => {
+ navigate(Path.McpMarket, { state: { fromHome: true } });
+ }}
+ shadow
+ />
+ )}
}
text={shouldNarrow ? undefined : Locale.Discovery.Name}
className={styles["sidebar-bar-button"]}
- onClick={() => setShowPluginSelector(true)}
+ onClick={() => setshowDiscoverySelector(true)}
shadow
/>
- {showPluginSelector && (
+ {showDiscoverySelector && (
{
+ ...DISCOVERY.map((item) => {
return {
title: item.name,
value: item.path,
};
}),
]}
- onClose={() => setShowPluginSelector(false)}
+ onClose={() => setshowDiscoverySelector(false)}
onSelection={(s) => {
navigate(s[0], { state: { fromHome: true } });
}}
@@ -286,7 +317,7 @@ export function SideBar(props: { className?: string }) {
-
+
}
onClick={async () => {
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index 4af37dbba1c..7b9f5ace028 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -23,6 +23,8 @@ import React, {
useRef,
} from "react";
import { IconButton } from "./button";
+import { Avatar } from "./emoji";
+import clsx from "clsx";
export function Popover(props: {
children: JSX.Element;
@@ -45,7 +47,7 @@ export function Popover(props: {
export function Card(props: { children: JSX.Element[]; className?: string }) {
return (
- {props.children}
+ {props.children}
);
}
@@ -60,11 +62,13 @@ export function ListItem(props: {
}) {
return (
@@ -135,9 +139,9 @@ export function Modal(props: ModalProps) {
return (
{props.title}
@@ -260,7 +264,7 @@ export function Input(props: InputProps) {
return (
);
}
@@ -301,9 +305,13 @@ export function Select(
const { className, children, align, ...otherProps } = props;
return (