mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 13:25:11 +00:00
Upgrade: New Home Screen for Khoj (#860)
* V1 of the new automations page Implemented: - Shareable - Editable - Suggested Cards - Create new cards - added side panel new conversation button - Implement mobile-friendly view for homepage - Fix issue of new conversations being created when selected agent is changed - Improve center of the homepage experience - Fix showing agent during first chat experience - dark mode gradient updates --------- Co-authored-by: sabaimran <narmiabas@gmail.com>
This commit is contained in:
@@ -71,8 +71,9 @@ async function openChat(slug: string, userData: UserProfile | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
||||||
|
const data = await response.json();
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
window.location.href = `/chat`;
|
window.location.href = `/chat?conversationId=${data.conversation_id}`;
|
||||||
} else if (response.status == 403 || response.status == 401) {
|
} else if (response.status == 403 || response.status == 401) {
|
||||||
window.location.href = unauthenticatedRedirectUrl;
|
window.location.href = unauthenticatedRedirectUrl;
|
||||||
} else {
|
} else {
|
||||||
@@ -294,7 +295,7 @@ export default function Agents() {
|
|||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<div className={`${styles.titleBar} text-5xl`}>
|
<div className={`${styles.titleBar} text-5xl`}>
|
||||||
Talk to a Specialized Agent
|
Agents
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.agentList}>
|
<div className={styles.agentList}>
|
||||||
Error loading agents
|
Error loading agents
|
||||||
@@ -307,7 +308,7 @@ export default function Agents() {
|
|||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<div className={`${styles.titleBar} text-5xl`}>
|
<div className={`${styles.titleBar} text-5xl`}>
|
||||||
Talk to a Specialized Agent
|
Agents
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.agentList}>
|
<div className={styles.agentList}>
|
||||||
<InlineLoading /> booting up your agents
|
<InlineLoading /> booting up your agents
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ div.main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestions {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
overflow-x: none;
|
||||||
|
height: 50%;
|
||||||
|
padding: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); */
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: center;
|
/* justify-content: center; */
|
||||||
}
|
}
|
||||||
|
|
||||||
div.inputBox {
|
div.inputBox {
|
||||||
@@ -124,3 +128,33 @@ div.agentIndicator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pink {
|
||||||
|
background-color: #f8d1f8;
|
||||||
|
color: #000000;
|
||||||
|
background-image: linear-gradient(to top, #f8d1f8 1%, white 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#blue {
|
||||||
|
background-color: #d1f8f8;
|
||||||
|
color: #000000;
|
||||||
|
background-image: linear-gradient(to top, #d1f8f8 1%, white 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#green {
|
||||||
|
background-color: #d1f8d1;
|
||||||
|
color: #000000;
|
||||||
|
background-image: linear-gradient(to top, #d1f8d1 1%, white 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#purple {
|
||||||
|
background-color: #f8d1f8;
|
||||||
|
color: #000000;
|
||||||
|
background-image: linear-gradient(to top, #f8d1f8 1%, white 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#yellow {
|
||||||
|
background-color: #f8f8d1;
|
||||||
|
color: #000000;
|
||||||
|
background-image: linear-gradient(to top, #f8f8d1 1%, white 100%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import styles from './chat.module.css';
|
import styles from './chat.module.css';
|
||||||
import React, { Suspense, useEffect, useState } from 'react';
|
import React, { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import SuggestionCard from '../components/suggestions/suggestionCard';
|
|
||||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||||
import ChatHistory from '../components/chatHistory/chatHistory';
|
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||||
import NavMenu from '../components/navMenu/navMenu';
|
import NavMenu from '../components/navMenu/navMenu';
|
||||||
@@ -19,9 +18,6 @@ import { welcomeConsole } from '../common/utils';
|
|||||||
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
||||||
import { useAuthenticatedData } from '../common/auth';
|
import { useAuthenticatedData } from '../common/auth';
|
||||||
|
|
||||||
|
|
||||||
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
|
||||||
|
|
||||||
interface ChatBodyDataProps {
|
interface ChatBodyDataProps {
|
||||||
chatOptionsData: ChatOptions | null;
|
chatOptionsData: ChatOptions | null;
|
||||||
setTitle: (title: string) => void;
|
setTitle: (title: string) => void;
|
||||||
@@ -40,10 +36,11 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
const [processingMessage, setProcessingMessage] = useState(false);
|
const [processingMessage, setProcessingMessage] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (conversationId) {
|
const storedMessage = localStorage.getItem("message");
|
||||||
props.onConversationIdChange?.(conversationId);
|
if (storedMessage) {
|
||||||
|
setMessage(storedMessage);
|
||||||
}
|
}
|
||||||
}, [conversationId, props.onConversationIdChange]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(message){
|
if(message){
|
||||||
@@ -52,11 +49,16 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
}
|
}
|
||||||
}, [message]);
|
}, [message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (conversationId) {
|
||||||
|
props.onConversationIdChange?.(conversationId);
|
||||||
|
}
|
||||||
|
}, [conversationId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.streamedMessages &&
|
if (props.streamedMessages &&
|
||||||
props.streamedMessages.length > 0 &&
|
props.streamedMessages.length > 0 &&
|
||||||
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||||
|
|
||||||
setProcessingMessage(false);
|
setProcessingMessage(false);
|
||||||
} else {
|
} else {
|
||||||
setMessage('');
|
setMessage('');
|
||||||
@@ -64,19 +66,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
}, [props.streamedMessages]);
|
}, [props.streamedMessages]);
|
||||||
|
|
||||||
if(!conversationId) {
|
if(!conversationId) {
|
||||||
return (
|
window.location.href = '/';
|
||||||
<div className={styles.suggestions}>
|
return;
|
||||||
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
|
||||||
<SuggestionCard
|
|
||||||
key={key}
|
|
||||||
title={`/${key}`}
|
|
||||||
body={value}
|
|
||||||
link='#' // replace with actual link if available
|
|
||||||
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -88,7 +79,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
pendingMessage={processingMessage ? message : ''}
|
pendingMessage={processingMessage ? message : ''}
|
||||||
incomingMessages={props.streamedMessages} />
|
incomingMessages={props.streamedMessages} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
|
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3 dark:bg-neutral-700 dark:border-0 dark:shadow-sm`}>
|
||||||
<ChatInputArea
|
<ChatInputArea
|
||||||
isLoggedIn={props.isLoggedIn}
|
isLoggedIn={props.isLoggedIn}
|
||||||
sendMessage={(message) => setMessage(message)}
|
sendMessage={(message) => setMessage(message)}
|
||||||
@@ -121,7 +112,6 @@ export default function Chat() {
|
|||||||
const handleWebSocketMessage = (event: MessageEvent) => {
|
const handleWebSocketMessage = (event: MessageEvent) => {
|
||||||
let chunk = event.data;
|
let chunk = event.data;
|
||||||
let currentMessage = messages.find(message => !message.completed);
|
let currentMessage = messages.find(message => !message.completed);
|
||||||
|
|
||||||
if (!currentMessage) {
|
if (!currentMessage) {
|
||||||
console.error("No current message found");
|
console.error("No current message found");
|
||||||
return;
|
return;
|
||||||
@@ -204,9 +194,20 @@ export default function Chat() {
|
|||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatWS) {
|
||||||
|
chatWS.onmessage = handleWebSocketMessage;
|
||||||
|
}
|
||||||
|
}, [chatWS, messages]);
|
||||||
|
|
||||||
|
//same as ChatBodyData for local storage message
|
||||||
|
useEffect(() => {
|
||||||
|
const storedMessage = localStorage.getItem("message");
|
||||||
|
setQueryToProcess(storedMessage || '');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatWS && queryToProcess) {
|
if (chatWS && queryToProcess) {
|
||||||
// Add a new object to the state
|
|
||||||
const newStreamMessage: StreamMessage = {
|
const newStreamMessage: StreamMessage = {
|
||||||
rawResponse: "",
|
rawResponse: "",
|
||||||
trainOfThought: [],
|
trainOfThought: [],
|
||||||
@@ -215,41 +216,51 @@ export default function Chat() {
|
|||||||
completed: false,
|
completed: false,
|
||||||
timestamp: (new Date()).toISOString(),
|
timestamp: (new Date()).toISOString(),
|
||||||
rawQuery: queryToProcess || "",
|
rawQuery: queryToProcess || "",
|
||||||
}
|
};
|
||||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||||
|
|
||||||
|
if (chatWS.readyState === WebSocket.OPEN) {
|
||||||
|
chatWS.send(queryToProcess);
|
||||||
setProcessQuerySignal(true);
|
setProcessQuerySignal(true);
|
||||||
} else {
|
|
||||||
if (!chatWS) {
|
|
||||||
console.error("No WebSocket connection available");
|
|
||||||
}
|
}
|
||||||
if (!queryToProcess) {
|
else {
|
||||||
console.error("No query to process");
|
console.error("WebSocket is not open. ReadyState:", chatWS.readyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setQueryToProcess('');
|
||||||
}
|
}
|
||||||
}, [queryToProcess]);
|
}, [queryToProcess, chatWS]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (processQuerySignal && chatWS) {
|
if (processQuerySignal && chatWS && chatWS.readyState === WebSocket.OPEN) {
|
||||||
setProcessQuerySignal(false);
|
setProcessQuerySignal(false);
|
||||||
chatWS.onmessage = handleWebSocketMessage;
|
chatWS.onmessage = handleWebSocketMessage;
|
||||||
chatWS?.send(queryToProcess);
|
chatWS.send(queryToProcess);
|
||||||
|
localStorage.removeItem("message");
|
||||||
}
|
}
|
||||||
}, [processQuerySignal]);
|
}, [processQuerySignal, chatWS]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
const setupWebSocketConnection = async () => {
|
||||||
if (conversationId) {
|
if (conversationId && (!chatWS || chatWS.readyState === WebSocket.CLOSED)) {
|
||||||
|
if(queryToProcess) {
|
||||||
|
const newWS = await setupWebSocket(conversationId, queryToProcess);
|
||||||
|
localStorage.removeItem("message");
|
||||||
|
setChatWS(newWS);
|
||||||
|
}
|
||||||
|
else {
|
||||||
const newWS = await setupWebSocket(conversationId);
|
const newWS = await setupWebSocket(conversationId);
|
||||||
setChatWS(newWS);
|
setChatWS(newWS);
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
};
|
||||||
|
setupWebSocketConnection();
|
||||||
}, [conversationId]);
|
}, [conversationId]);
|
||||||
|
|
||||||
const handleConversationIdChange = (newConversationId: string) => {
|
const handleConversationIdChange = (newConversationId: string) => {
|
||||||
setConversationID(newConversationId);
|
setConversationID(newConversationId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -260,7 +271,7 @@ export default function Chat() {
|
|||||||
<title>
|
<title>
|
||||||
{title}
|
{title}
|
||||||
</title>
|
</title>
|
||||||
<div className={styles.sidePanel}>
|
<div>
|
||||||
<SidePanel
|
<SidePanel
|
||||||
webSocketConnected={chatWS !== null}
|
webSocketConnected={chatWS !== null}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ export const setupWebSocket = async (conversationId: string, initialMessage?: st
|
|||||||
|
|
||||||
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
|
let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`;
|
||||||
|
|
||||||
if (conversationId === null) return null;
|
if (conversationId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
webSocketUrl += `?conversation_id=${conversationId}`;
|
webSocketUrl += `?conversation_id=${conversationId}`;
|
||||||
|
|||||||
60
src/interface/web/app/common/colorUtils.ts
Normal file
60
src/interface/web/app/common/colorUtils.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export function convertColorToTextClass(color: string) {
|
||||||
|
if (color === 'red') return `text-red-500`;
|
||||||
|
if (color === 'yellow') return `text-yellow-500`;
|
||||||
|
if (color === 'green') return `text-green-500`;
|
||||||
|
if (color === 'blue') return `text-blue-500`;
|
||||||
|
if (color === 'orange') return `text-orange-500`;
|
||||||
|
if (color === 'purple') return `text-purple-500`;
|
||||||
|
if (color === 'pink') return `text-pink-500`;
|
||||||
|
if (color === 'teal') return `text-teal-500`;
|
||||||
|
if (color === 'cyan') return `text-cyan-500`;
|
||||||
|
if (color === 'lime') return `text-lime-500`;
|
||||||
|
if (color === 'indigo') return `text-indigo-500`;
|
||||||
|
if (color === 'fuschia') return `text-fuschia-500`;
|
||||||
|
if (color === 'rose') return `text-rose-500`;
|
||||||
|
if (color === 'sky') return `text-sky-500`;
|
||||||
|
if (color === 'amber') return `text-amber-500`;
|
||||||
|
if (color === 'emerald') return `text-emerald-500`;
|
||||||
|
return `text-gray-500`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertSuggestionColorToTextClass(color: string) {
|
||||||
|
const colors = ['blue', 'yellow', 'green', 'pink', 'purple'];
|
||||||
|
if (colors.includes(color)) {
|
||||||
|
return "" + `bg-gradient-to-b from-[hsl(var(--background))] to-${color}-100/${color == "green" ? "90" : "70"} dark:from-[hsl(var(--background))] dark:to-${color}-950/30 dark:border dark:border-neutral-700`;
|
||||||
|
}
|
||||||
|
return `bg-gradient-to-b from-white to-orange-50`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertColorToBorderClass(color: string) {
|
||||||
|
console.log("Color:", color);
|
||||||
|
if (color === 'red') return `border-red-500`;
|
||||||
|
if (color === 'yellow') return `border-yellow-500`;
|
||||||
|
if (color === 'green') return `border-green-500`;
|
||||||
|
if (color === 'blue') return `border-blue-500`;
|
||||||
|
if (color === 'orange') return `border-orange-500`;
|
||||||
|
if (color === 'purple') return `border-purple-500`;
|
||||||
|
if (color === 'pink') return `border-pink-500`;
|
||||||
|
if (color === 'teal') return `border-teal-500`;
|
||||||
|
if (color === 'cyan') return `border-cyan-500`;
|
||||||
|
if (color === 'lime') return `border-lime-500`;
|
||||||
|
if (color === 'indigo') return `border-indigo-500`;
|
||||||
|
if (color === 'fuschia') return `border-fuschia-500`;
|
||||||
|
if (color === 'rose') return `border-rose-500`;
|
||||||
|
if (color === 'sky') return `border-sky-500`;
|
||||||
|
if (color === 'amber') return `border-amber-500`;
|
||||||
|
if (color === 'emerald') return `border-emerald-500`;
|
||||||
|
return `border-gray-500`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const colorMap: Record<string, string> = {
|
||||||
|
'red': 'border-red-500',
|
||||||
|
'blue': 'border-blue-500',
|
||||||
|
'green': 'border-green-500',
|
||||||
|
'yellow': 'border-yellow-500',
|
||||||
|
'purple': 'border-purple-500',
|
||||||
|
'pink': 'border-pink-500',
|
||||||
|
'indigo': 'border-indigo-500',
|
||||||
|
'gray': 'border-gray-500',
|
||||||
|
'orange': 'border-orange-500',
|
||||||
|
};
|
||||||
53
src/interface/web/app/common/iconUtils.tsx
Normal file
53
src/interface/web/app/common/iconUtils.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { convertColorToTextClass } from './colorUtils';
|
||||||
|
import {
|
||||||
|
Lightbulb,
|
||||||
|
Robot,
|
||||||
|
Aperture,
|
||||||
|
GraduationCap,
|
||||||
|
Jeep,
|
||||||
|
Island,
|
||||||
|
MathOperations,
|
||||||
|
Asclepius,
|
||||||
|
Couch,
|
||||||
|
Code,
|
||||||
|
Atom,
|
||||||
|
ClockCounterClockwise,
|
||||||
|
PaperPlaneTilt,
|
||||||
|
Info,
|
||||||
|
UserCircle,
|
||||||
|
Globe,
|
||||||
|
Palette,
|
||||||
|
LinkBreak,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
interface IconMap {
|
||||||
|
[key: string]: (color: string, width: string, height: string) => JSX.Element | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap: IconMap = {
|
||||||
|
Lightbulb: (color: string, width: string, height: string) => <Lightbulb className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Robot: (color: string, width: string, height: string) => <Robot className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Aperture: (color: string, width: string, height: string) => <Aperture className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
GraduationCap: (color: string, width: string, height: string) => <GraduationCap className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Jeep: (color: string, width: string, height: string) => <Jeep className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Island: (color: string, width: string, height: string) => <Island className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
MathOperations: (color: string, width: string, height: string) => <MathOperations className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Asclepius: (color: string, width: string, height: string) => <Asclepius className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Couch: (color: string, width: string, height: string) => <Couch className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Code: (color: string, width: string, height: string) => <Code className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Atom: (color: string, width: string, height: string) => <Atom className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
ClockCounterClockwise: (color: string, width: string, height: string) => <ClockCounterClockwise className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Globe: (color: string, width: string, height: string) => <Globe className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
Palette: (color: string, width: string, height: string) => <Palette className={`${width} ${height} ${color} mr-2`} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIconFromIconName(iconName: string, color: string = 'gray', width: string = 'w-6', height: string = 'h-6') {
|
||||||
|
const icon = iconMap[iconName];
|
||||||
|
const colorName = color.toLowerCase();
|
||||||
|
const colorClass = convertColorToTextClass(colorName);
|
||||||
|
|
||||||
|
return icon ? icon(colorClass, width, height) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getIconFromIconName };
|
||||||
@@ -177,7 +177,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((chatData: ChatResponse) => {
|
.then((chatData: ChatResponse) => {
|
||||||
props.setTitle(chatData.response.slug);
|
props.setTitle(chatData.response.slug);
|
||||||
if (chatData && chatData.response && chatData.response.chat.length > 0) {
|
if (chatData && chatData.response && chatData.response.chat && chatData.response.chat.length > 0) {
|
||||||
|
|
||||||
if (chatData.response.chat.length === data?.chat.length) {
|
if (chatData.response.chat.length === data?.chat.length) {
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
@@ -192,7 +192,18 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
}
|
}
|
||||||
setFetchingData(false);
|
setFetchingData(false);
|
||||||
} else {
|
} else {
|
||||||
|
if (chatData.response.agent && chatData.response.conversation_id) {
|
||||||
|
const chatMetadata ={
|
||||||
|
chat: [],
|
||||||
|
agent: chatData.response.agent,
|
||||||
|
conversation_id: chatData.response.conversation_id,
|
||||||
|
slug: chatData.response.slug,
|
||||||
|
}
|
||||||
|
setData(chatMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
|
setFetchingData(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -241,7 +252,6 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
if (!props.conversationId && !props.publicConversationSlug) {
|
if (!props.conversationId && !props.publicConversationSlug) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className={`h-[80vh]`}>
|
<ScrollArea className={`h-[80vh]`}>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
@@ -334,7 +344,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
<ProfileCard
|
<ProfileCard
|
||||||
name={constructAgentName()}
|
name={constructAgentName()}
|
||||||
link={constructAgentLink()}
|
link={constructAgentLink()}
|
||||||
avatar={<Lightbulb color='orange' weight='fill' className="mt-1 mx-1" />}
|
avatar={<Lightbulb color='orange' weight='fill' />}
|
||||||
description={constructAgentPersona()}
|
description={constructAgentPersona()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,29 @@ interface ChatInputProps {
|
|||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewConvo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/chat/sessions', { method: "POST" });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const conversationID = data.conversation_id;
|
||||||
|
|
||||||
|
if (!conversationID) {
|
||||||
|
throw new Error("Conversation ID not found in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `/chat?conversationId=${conversationID}`;
|
||||||
|
return url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating new conversation:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatInputArea(props: ChatInputProps) {
|
export default function ChatInputArea(props: ChatInputProps) {
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -270,7 +293,7 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className={`${styles.actualInputArea} flex items-center justify-between`}>
|
<div className={`${styles.actualInputArea} flex items-center justify-between dark:bg-neutral-700`}>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
@@ -283,11 +306,11 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||||
disabled={props.sendDisabled}
|
disabled={props.sendDisabled}
|
||||||
onClick={handleFileButtonClick}>
|
onClick={handleFileButtonClick}>
|
||||||
<FileArrowUp weight='fill' />
|
<FileArrowUp weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="grid w-full gap-1.5 relative">
|
<div className="grid w-full gap-1.5 relative">
|
||||||
<Textarea
|
<Textarea
|
||||||
className='border-none w-full h-16 min-h-16 md:py-4 rounded-lg text-lg'
|
className={`border-none w-full h-16 min-h-16 md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? 'text-md' : 'text-lg'}`}
|
||||||
placeholder="Type / to see a list of commands"
|
placeholder="Type / to see a list of commands"
|
||||||
id="message"
|
id="message"
|
||||||
value={message}
|
value={message}
|
||||||
@@ -304,13 +327,13 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||||
disabled={props.sendDisabled}>
|
disabled={props.sendDisabled}>
|
||||||
<Microphone weight='fill' />
|
<Microphone weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
|
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
|
||||||
onClick={onSendMessage}
|
onClick={onSendMessage}
|
||||||
disabled={props.sendDisabled}>
|
disabled={props.sendDisabled}>
|
||||||
<ArrowCircleUp />
|
<ArrowCircleUp className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -116,13 +116,13 @@ export default function NavMenu(props: NavMenuProps) {
|
|||||||
<DropdownMenuTrigger>=</DropdownMenuTrigger>
|
<DropdownMenuTrigger>=</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>Chat</Link>
|
<Link href='/' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background no-underline`}>Chat</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>Agents</Link>
|
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background no-underline`}>Agents</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>Automations</Link>
|
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background no-underline`}>Automations</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{userData && <>
|
{userData && <>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -142,17 +142,17 @@ export default function NavMenu(props: NavMenuProps) {
|
|||||||
:
|
:
|
||||||
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
|
<Menubar className='items-top inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground'>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||||
<MenubarTrigger>Chat</MenubarTrigger>
|
<MenubarTrigger>Chat</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||||
<MenubarTrigger>Agents</MenubarTrigger>
|
<MenubarTrigger>Agents</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background no-underline`}>
|
||||||
<MenubarTrigger>Automations</MenubarTrigger>
|
<MenubarTrigger>Automations</MenubarTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ArrowRight } from '@phosphor-icons/react';
|
import { ArrowRight } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
avatar: JSX.Element;
|
avatar: JSX.Element;
|
||||||
@@ -11,24 +20,37 @@ interface ProfileCardProps {
|
|||||||
const ProfileCard: React.FC<ProfileCardProps> = ({ name, avatar, link, description }) => {
|
const ProfileCard: React.FC<ProfileCardProps> = ({ name, avatar, link, description }) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative group flex">
|
<div className="relative group flex">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" className="flex items-center justify-center gap-2">
|
||||||
{avatar}
|
{avatar}
|
||||||
<span>{name}</span>
|
<div>{name}</div>
|
||||||
<div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
</Button>
|
||||||
<div className="flex items-center">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className='w-80 h-30'>
|
||||||
|
{/* <div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"> */}
|
||||||
|
<a href={link} target="_blank" rel="noreferrer" className="mt-1 ml-2 block no-underline">
|
||||||
|
<div className="flex items-center justify-start gap-2">
|
||||||
{avatar}
|
{avatar}
|
||||||
<span className="mr-2 mt-1 flex">
|
<div className="mr-2 mt-1 flex justify-center items-center text-sm font-semibold text-gray-800">
|
||||||
{name}
|
{name}
|
||||||
<a href={link} target="_blank" rel="noreferrer" className="mt-1 ml-2 block">
|
<ArrowRight weight="bold" className='ml-1' />
|
||||||
<ArrowRight weight="bold"/>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
||||||
{description || 'A Khoj agent'}
|
{description || 'A Khoj agent'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus } from "@phosphor-icons/react";
|
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus, Sidebar, NotePencil } from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface ChatHistory {
|
interface ChatHistory {
|
||||||
conversation_id: string;
|
conversation_id: string;
|
||||||
@@ -702,8 +702,14 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
|
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed}`}>
|
||||||
<div className="flex items-center justify-between">
|
<div className={`flex items-center justify-between ${(enabled || props.isMobileWidth) ? 'flex-row' : 'flex-col'}`}>
|
||||||
<img src="/khoj-logo.svg" alt="logo" className="w-16"/>
|
<Link href='/'>
|
||||||
|
<img
|
||||||
|
src="/khoj-logo.svg"
|
||||||
|
alt="khoj logo"
|
||||||
|
width={52}
|
||||||
|
height={52} />
|
||||||
|
</Link>
|
||||||
{
|
{
|
||||||
authenticatedData && props.isMobileWidth ?
|
authenticatedData && props.isMobileWidth ?
|
||||||
<Drawer open={enabled} onOpenChange={(open) => {
|
<Drawer open={enabled} onOpenChange={(open) => {
|
||||||
@@ -711,7 +717,7 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
setEnabled(open);
|
setEnabled(open);
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
<DrawerTrigger><ArrowRight className="h-4 w-4 mx-2" weight="bold"/></DrawerTrigger>
|
<DrawerTrigger><Sidebar className="h-4 w-4 mx-2" weight="thin" /></DrawerTrigger>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader>
|
<DrawerHeader>
|
||||||
<DrawerTitle>Sessions and Files</DrawerTitle>
|
<DrawerTitle>Sessions and Files</DrawerTitle>
|
||||||
@@ -738,9 +744,14 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
:
|
:
|
||||||
|
<div className={`flex items-center ${enabled ? 'flex-row gap-2' : 'flex-col pt-2'}`}>
|
||||||
|
<Link className={` ${enabled ? 'ml-2' : ''}`} href="/">
|
||||||
|
{enabled ? <NotePencil className="h-6 w-6" /> : <NotePencil className="h-6 w-6" color="gray" />}
|
||||||
|
</Link>
|
||||||
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
||||||
{enabled ? <ArrowLeft className="h-4 w-4" weight="bold"/> : <ArrowRight className="h-4 w-4 mx-2" weight="bold"/>}
|
{enabled ? <Sidebar className="h-6 w-6" /> : <Sidebar className="h-6 w-6" color="gray" />}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ div.expanded {
|
|||||||
div.collapsed {
|
div.collapsed {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
height: fit-content;
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.session {
|
p.session {
|
||||||
@@ -119,8 +124,8 @@ div.modalSessionsList div.session {
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
div.panel {
|
div.panel {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
width: 100%;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.expanded {
|
div.expanded {
|
||||||
|
|||||||
@@ -1,32 +1,53 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
|
||||||
import styles from "./suggestions.module.css";
|
import styles from "./suggestions.module.css";
|
||||||
|
|
||||||
|
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||||
|
|
||||||
|
|
||||||
|
function convertSuggestionColorToIconClass(color: string) {
|
||||||
|
if (color.includes('blue')) return getIconFromIconName('Robot', 'blue', 'w-8', 'h-8');
|
||||||
|
if (color.includes('yellow')) return getIconFromIconName('Globe', 'yellow', 'w-8', 'h-8');
|
||||||
|
if (color.includes('green')) return getIconFromIconName('Palette', 'green', 'w-8', 'h-8');
|
||||||
|
else return getIconFromIconName('Lightbulb', 'orange', 'w-8', 'h-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface SuggestionCardProps {
|
interface SuggestionCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
link: string;
|
link: string;
|
||||||
styleClass: string;
|
image: string;
|
||||||
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SuggestionCard(data: SuggestionCardProps) {
|
export default function SuggestionCard(data: SuggestionCardProps) {
|
||||||
|
const cardClassName = `${styles.card} ${data.color} md:w-full md:h-fit sm:w-full sm:h-fit lg:w-[200px] lg:h-[200px]`;
|
||||||
|
const titleClassName = `${styles.title} pt-2 dark:text-white dark:font-bold`;
|
||||||
|
const descriptionClassName = `${styles.text} dark:text-white`;
|
||||||
|
|
||||||
return (
|
const cardContent = (
|
||||||
<div className={styles[data.styleClass] + " " + styles.card}>
|
<Card className={cardClassName}>
|
||||||
<div className={styles.title}>
|
<CardHeader className="m-0 p-2 pb-1 relative">
|
||||||
{data.title}
|
{convertSuggestionColorToIconClass(data.image)}
|
||||||
</div>
|
<CardTitle className={titleClassName}>{data.title}</CardTitle>
|
||||||
<div className={styles.body}>
|
</CardHeader>
|
||||||
{data.body}
|
<CardContent className="m-0 p-2 pr-4 pt-1">
|
||||||
</div>
|
<CardDescription className={descriptionClassName}>{data.body}</CardDescription>
|
||||||
<div>
|
</CardContent>
|
||||||
<a
|
</Card>
|
||||||
href={data.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>click me
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return data.link ? (
|
||||||
|
<a href={data.link} className="no-underline">
|
||||||
|
{cardContent}
|
||||||
|
</a>
|
||||||
|
) : cardContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,18 @@
|
|||||||
div.pink {
|
|
||||||
background-color: #f8d1f8;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.blue {
|
.card {
|
||||||
background-color: #d1f8f8;
|
padding: 0.5rem;
|
||||||
color: #000000;
|
margin: 0.05rem;
|
||||||
}
|
|
||||||
|
|
||||||
div.green {
|
|
||||||
background-color: #d1f8d1;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.purple {
|
|
||||||
background-color: #f8d1f8;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.yellow {
|
|
||||||
background-color: #f8f8d1;
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.card {
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1rem;
|
|
||||||
border: 1px solid #000000;
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.title {
|
.title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.0rem;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.body {
|
.text {
|
||||||
font-size: 1rem;
|
padding-top: 0.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: wrap;
|
||||||
|
padding-right: 4px;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import "./globals.css";
|
|||||||
const inter = Noto_Sans({ subsets: ["latin"] });
|
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Khoj AI",
|
title: "Khoj AI - Chat",
|
||||||
description: "An AI copilot for your second brain",
|
description: "Use this page to chat with Khoj AI.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -16,7 +16,18 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>{children}</body>
|
<meta httpEquiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self' https://assets.khoj.dev;
|
||||||
|
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||||
|
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||||
|
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
|
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||||
|
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||||
|
child-src 'none';
|
||||||
|
object-src 'none';"></meta>
|
||||||
|
<body className={inter.className}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,224 +1,121 @@
|
|||||||
.main {
|
div.main {
|
||||||
|
height: 100vh;
|
||||||
|
color: hsla(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
overflow-x: none;
|
||||||
justify-content: space-between;
|
height: fit-content;
|
||||||
align-items: center;
|
padding: 10px;
|
||||||
padding: 6rem;
|
white-space: nowrap;
|
||||||
min-height: 100vh;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
div.inputBox {
|
||||||
display: inherit;
|
border: 1px solid var(--border-color);
|
||||||
justify-content: inherit;
|
border-radius: 16px;
|
||||||
align-items: inherit;
|
box-shadow: 0 4px 10px var(--box-shadow-color);
|
||||||
font-size: 0.85rem;
|
margin-bottom: 20px;
|
||||||
max-width: var(--max-width);
|
gap: 12px;
|
||||||
width: 100%;
|
align-content: center;
|
||||||
z-index: 2;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description a {
|
input.inputBox {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p {
|
|
||||||
position: relative;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: rgba(var(--callout-rgb), 0.5);
|
|
||||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
font-weight: 700;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
|
||||||
max-width: 100%;
|
|
||||||
width: var(--max-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1rem 1.2rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: rgba(var(--card-rgb), 0);
|
|
||||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
|
||||||
transition: background 200ms, border 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card span {
|
|
||||||
display: inline-block;
|
|
||||||
transition: transform 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin: 0;
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
max-width: 30ch;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 4rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before {
|
|
||||||
background: var(--secondary-glow);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 480px;
|
|
||||||
height: 360px;
|
|
||||||
margin-left: -400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::after {
|
|
||||||
background: var(--primary-glow);
|
|
||||||
width: 240px;
|
|
||||||
height: 180px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before,
|
|
||||||
.center::after {
|
|
||||||
content: "";
|
|
||||||
left: 50%;
|
|
||||||
position: absolute;
|
|
||||||
filter: blur(45px);
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enable hover only on non-touch devices */
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
|
||||||
.card:hover {
|
|
||||||
background: rgba(var(--card-rgb), 0.1);
|
|
||||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover span {
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
|
||||||
.card:hover span {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.content {
|
|
||||||
padding: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
margin-bottom: 120px;
|
|
||||||
max-width: 320px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1rem 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
padding: 8rem 0 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center::before {
|
|
||||||
transform: none;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description a {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p,
|
|
||||||
.description div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description p {
|
|
||||||
align-items: center;
|
|
||||||
inset: 0 0 auto;
|
|
||||||
padding: 2rem 1rem 1.4rem;
|
|
||||||
border-radius: 0;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
|
||||||
background: linear-gradient(to bottom,
|
|
||||||
rgba(var(--background-start-rgb), 1),
|
|
||||||
rgba(var(--callout-rgb), 0.5));
|
|
||||||
background-clip: padding-box;
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description div {
|
input.inputBox:focus {
|
||||||
align-items: flex-end;
|
outline: none;
|
||||||
pointer-events: none;
|
background-color: transparent;
|
||||||
inset: auto 0 0;
|
}
|
||||||
padding: 2rem;
|
|
||||||
height: 200px;
|
div.inputBox:focus {
|
||||||
background: linear-gradient(to bottom,
|
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||||
transparent 0%,
|
}
|
||||||
rgb(var(--background-end-rgb)) 40%);
|
|
||||||
z-index: 1;
|
div.chatBodyFull {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.inputBox {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: linear-gradient(var(--calm-green), var(--calm-blue));
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputBox {
|
||||||
|
color: hsla(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatLayout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.titleBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBoxBody {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.sidePanel {
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
div.chatBody {
|
||||||
|
grid-template-columns: 0fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tablet and Smaller Desktop */
|
@media screen and (max-width: 768px) {
|
||||||
@media (min-width: 701px) and (max-width: 1120px) {
|
div.inputBox {
|
||||||
.grid {
|
margin-bottom: 0px;
|
||||||
grid-template-columns: repeat(2, 50%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
div.chatBoxBody {
|
||||||
.logo {
|
width: 100%;
|
||||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
div.chatBox {
|
||||||
from {
|
padding: 0;
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
div.chatLayout {
|
||||||
transform: rotate(0deg);
|
gap: 0;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,355 @@
|
|||||||
import styles from "./page.module.css";
|
'use client'
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
import styles from './page.module.css';
|
||||||
|
import React, { Suspense, useEffect, useState, useMemo } from 'react';
|
||||||
|
|
||||||
|
import SuggestionCard from './components/suggestions/suggestionCard';
|
||||||
|
import SidePanel from './components/sidePanel/chatHistorySidePanel';
|
||||||
|
import NavMenu from './components/navMenu/navMenu';
|
||||||
|
import Loading from './components/loading/loading';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
import { StreamMessage } from './components/chatMessage/chatMessage';
|
||||||
|
import ChatInputArea, { ChatOptions } from './components/chatInputArea/chatInputArea';
|
||||||
|
import { useAuthenticatedData } from './common/auth';
|
||||||
|
import { Card, CardContent, CardTitle } from '@/components/ui/card';
|
||||||
|
import { convertSuggestionColorToTextClass, colorMap, convertColorToBorderClass } from './common/colorUtils';
|
||||||
|
import { getIconFromIconName } from './common/iconUtils';
|
||||||
|
import { ClockCounterClockwise } from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
//samples for suggestion cards (should be moved to json later)
|
||||||
|
const suggestions: Suggestion[] = [["Automation", "blue", "Send me a summary of HackerNews every morning.", "/automations?subject=Summarizing%20Top%20Headlines%20from%20HackerNews&query=Summarize%20the%20top%20headlines%20on%20HackerNews&crontime=00%207%20*%20*%20*"], ["Automation", "blue", "Compose a bedtime story that a five-year-old might enjoy.", "/automations?subject=Daily%20Bedtime%20Story&query=Compose%20a%20bedtime%20story%20that%20a%20five-year-old%20might%20enjoy.%20It%20should%20not%20exceed%20five%20paragraphs.%20Appeal%20to%20the%20imagination%2C%20but%20weave%20in%20learnings.&crontime=0%2021%20*%20*%20*"], ["Paint", "green", "Paint a picture of a sunset but it's made of stained glass tiles", ""], ["Online Search", "yellow", "Search for the best attractions in Austria Hungary", ""]];
|
||||||
|
|
||||||
|
export interface AgentData {
|
||||||
|
slug: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
personality: string;
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatBodyDataProps {
|
||||||
|
chatOptionsData: ChatOptions | null;
|
||||||
|
setTitle: (title: string) => void;
|
||||||
|
onConversationIdChange?: (conversationId: string) => void;
|
||||||
|
setQueryToProcess: (query: string) => void;
|
||||||
|
streamedMessages: StreamMessage[];
|
||||||
|
setUploadedFiles: (files: string[]) => void;
|
||||||
|
isMobileWidth?: boolean;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
conversationId: string | null; // Added this line
|
||||||
|
}
|
||||||
|
type Suggestion = [string, string, string, string];
|
||||||
|
|
||||||
|
async function createNewConvo(slug: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, { method: "POST" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const conversationID = data.conversation_id;
|
||||||
|
if (!conversationID) {
|
||||||
|
throw new Error("Conversation ID not found in response");
|
||||||
|
}
|
||||||
|
return conversationID;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating new conversation:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatBodyData(props: ChatBodyDataProps) {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [processingMessage, setProcessingMessage] = useState(false);
|
||||||
|
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
|
||||||
|
const [shuffledColors, setShuffledColors] = useState<string[]>([]);
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
|
||||||
|
|
||||||
|
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
|
||||||
|
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
|
||||||
|
|
||||||
|
function shuffleAndSetOptions() {
|
||||||
|
const shuffled = [...suggestions].sort(() => 0.5 - Math.random());
|
||||||
|
setShuffledOptions(shuffled.slice(0, 3));
|
||||||
|
//use the text to color function above convertSuggestionColorToTextClass
|
||||||
|
const colors = shuffled.map(option => convertSuggestionColorToTextClass(option[1]));
|
||||||
|
setShuffledColors(colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.chatOptionsData) {
|
||||||
|
shuffleAndSetOptions();
|
||||||
|
}
|
||||||
|
}, [props.chatOptionsData]);
|
||||||
|
|
||||||
|
function onButtonClick() {
|
||||||
|
shuffleAndSetOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processMessage = async () => {
|
||||||
|
if (message && !processingMessage) {
|
||||||
|
setProcessingMessage(true);
|
||||||
|
try {
|
||||||
|
const newConversationId = await createNewConvo(selectedAgent || "khoj");
|
||||||
|
props.onConversationIdChange?.(newConversationId);
|
||||||
|
window.location.href = `/chat?conversationId=${newConversationId}`;
|
||||||
|
localStorage.setItem('message', message);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error creating new conversation:", error);
|
||||||
|
setProcessingMessage(false);
|
||||||
|
}
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
processMessage();
|
||||||
|
if (message) {
|
||||||
|
setProcessingMessage(true);
|
||||||
|
props.setQueryToProcess(message);
|
||||||
|
};
|
||||||
|
}, [selectedAgent, message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.streamedMessages &&
|
||||||
|
props.streamedMessages.length > 0 &&
|
||||||
|
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||||
|
setProcessingMessage(false);
|
||||||
|
} else {
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
}, [props.streamedMessages]);
|
||||||
|
|
||||||
|
const nSlice = props.isMobileWidth ? 3 : 4;
|
||||||
|
|
||||||
|
const agents = data ? data.slice(0, nSlice) : []; //select first 4 agents to show as options
|
||||||
|
|
||||||
|
//generate colored icons for the selected agents
|
||||||
|
const agentIcons = agents.map(agent => getIconFromIconName(agent.icon, agent.color) || <Image key={agent.name} src={agent.avatar} alt={agent.name} width={50} height={50} />);
|
||||||
|
function fillArea(link: string, type: string, prompt: string) {
|
||||||
|
if (!link) {
|
||||||
|
let message_str = "";
|
||||||
|
prompt = prompt.charAt(0).toLowerCase() + prompt.slice(1);
|
||||||
|
|
||||||
|
if (type === "Online Search") {
|
||||||
|
message_str = "/online " + prompt;
|
||||||
|
} else if (type === "Paint") {
|
||||||
|
message_str = "/paint " + prompt;
|
||||||
|
} else {
|
||||||
|
message_str = prompt;
|
||||||
|
}
|
||||||
|
// Get the textarea element
|
||||||
|
const message_area = document.getElementById("message") as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
if (message_area) {
|
||||||
|
// Update the value directly
|
||||||
|
message_area.value = message_str;
|
||||||
|
setMessage(message_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTailwindBorderClass(color: string): string {
|
||||||
|
return colorMap[color] || 'border-black'; // Default to black if color not found
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightHandler(slug: string): void {
|
||||||
|
const buttons = document.getElementsByClassName("agent");
|
||||||
|
const agent = agents.find(agent => agent.slug === slug);
|
||||||
|
const borderColorClass = getTailwindBorderClass(agent?.color || 'gray');
|
||||||
|
|
||||||
|
Array.from(buttons).forEach((button: Element) => {
|
||||||
|
const buttonElement = button as HTMLElement;
|
||||||
|
if (buttonElement.classList.contains(slug)) {
|
||||||
|
buttonElement.classList.add(borderColorClass, 'border');
|
||||||
|
buttonElement.classList.remove('border-stone-100', 'dark:border-neutral-700');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Object.values(colorMap).forEach(colorClass => {
|
||||||
|
buttonElement.classList.remove(colorClass, 'border');
|
||||||
|
});
|
||||||
|
buttonElement.classList.add('border', 'border-stone-100', 'dark:border-neutral-700');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<main className={`${styles.main} text-3xl font-bold underline`}>
|
<div className={`${styles.chatBoxBody}`}>
|
||||||
Hi, Khoj here.
|
<div className="w-full text-center">
|
||||||
</main>
|
<div className="items-center">
|
||||||
|
<h1 className="text-center pb-6 px-4">What would you like to do?</h1>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
!props.isMobileWidth &&
|
||||||
|
<div className="flex pb-6 gap-2 items-center justify-center">
|
||||||
|
{agentIcons.map((icon, index) => (
|
||||||
|
<Card
|
||||||
|
key={`${index}-${agents[index].slug}`}
|
||||||
|
className={
|
||||||
|
`${selectedAgent === agents[index].slug ?
|
||||||
|
convertColorToBorderClass(agents[index].color) : 'border-stone-100 text-muted-foreground'}
|
||||||
|
hover:cursor-pointer rounded-lg px-2 py-2`}>
|
||||||
|
<CardTitle
|
||||||
|
className='text-center text-md font-medium flex justify-center items-center'
|
||||||
|
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||||
|
{icon} {agents[index].name}
|
||||||
|
</CardTitle>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||||
|
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">See All →</CardTitle>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={`${props.isMobileWidth} ? 'w-full' : 'w-fit`}>
|
||||||
|
{
|
||||||
|
!props.isMobileWidth &&
|
||||||
|
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center p-3 dark:bg-neutral-700`}>
|
||||||
|
<ChatInputArea
|
||||||
|
isLoggedIn={props.isLoggedIn}
|
||||||
|
sendMessage={(message) => setMessage(message)}
|
||||||
|
sendDisabled={processingMessage}
|
||||||
|
chatOptionsData={props.chatOptionsData}
|
||||||
|
conversationId={null}
|
||||||
|
isMobileWidth={props.isMobileWidth}
|
||||||
|
setUploadedFiles={props.setUploadedFiles} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className={`suggestions ${styles.suggestions} w-full ${props.isMobileWidth ? 'flex flex-col' : 'flex flex-row'} justify-center items-center`}>
|
||||||
|
{shuffledOptions.map(([key, styleClass, value, link], index) => (
|
||||||
|
<div key={key} onClick={() => fillArea(link, key, value)}>
|
||||||
|
<SuggestionCard
|
||||||
|
key={key + Math.random()}
|
||||||
|
title={key}
|
||||||
|
body={value.length > 65 ? value.substring(0, 65) + '...' : value}
|
||||||
|
link={link}
|
||||||
|
color={shuffledColors[index]}
|
||||||
|
image={shuffledColors[index]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center margin-auto">
|
||||||
|
<button
|
||||||
|
onClick={onButtonClick}
|
||||||
|
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700">
|
||||||
|
More Examples <ClockCounterClockwise className='h-4 w-4 inline' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
props.isMobileWidth &&
|
||||||
|
<div className={`${styles.inputBox} dark:bg-neutral-700 bg-background dark: align-middle items-center justify-center py-3 px-1`}>
|
||||||
|
<ChatInputArea
|
||||||
|
isLoggedIn={props.isLoggedIn}
|
||||||
|
sendMessage={(message) => setMessage(message)}
|
||||||
|
sendDisabled={processingMessage}
|
||||||
|
chatOptionsData={props.chatOptionsData}
|
||||||
|
conversationId={null}
|
||||||
|
isMobileWidth={props.isMobileWidth}
|
||||||
|
setUploadedFiles={props.setUploadedFiles} />
|
||||||
|
<div className="flex gap-2 items-center justify-left pt-4">
|
||||||
|
{agentIcons.map((icon, index) => (
|
||||||
|
<Card
|
||||||
|
key={`${index}-${agents[index].slug}`}
|
||||||
|
className={
|
||||||
|
`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : 'border-muted text-muted-foreground'} hover:cursor-pointer`
|
||||||
|
}>
|
||||||
|
<CardTitle
|
||||||
|
className='text-center text-xs font-medium flex justify-center items-center px-1.5 py-2'
|
||||||
|
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||||
|
{icon} {agents[index].name}
|
||||||
|
</CardTitle>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||||
|
<CardTitle className={`text-center ${props.isMobileWidth ? 'text-xs' : 'text-md'} font-normal flex justify-center items-center px-1.5 py-2`}>See All →</CardTitle>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||||
|
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||||
|
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||||
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
|
const authenticatedData = useAuthenticatedData();
|
||||||
|
|
||||||
|
const handleConversationIdChange = (newConversationId: string) => {
|
||||||
|
setConversationID(newConversationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/chat/options')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: ChatOptions) => {
|
||||||
|
setLoading(false);
|
||||||
|
if (data) {
|
||||||
|
setChatOptionsData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsMobileWidth(window.innerWidth < 786);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
setIsMobileWidth(window.innerWidth < 786);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||||
|
<title>
|
||||||
|
{title}
|
||||||
|
</title>
|
||||||
|
<div className={`${styles.sidePanel}`}>
|
||||||
|
<SidePanel
|
||||||
|
webSocketConnected={true}
|
||||||
|
conversationId={conversationId}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
isMobileWidth={isMobileWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.chatBox}`}>
|
||||||
|
<NavMenu selected="Chat" title={title}></NavMenu>
|
||||||
|
<div className={`${styles.chatBoxBody}`}>
|
||||||
|
<ChatBodyData
|
||||||
|
isLoggedIn={authenticatedData !== null}
|
||||||
|
streamedMessages={messages}
|
||||||
|
chatOptionsData={chatOptionsData}
|
||||||
|
setTitle={setTitle}
|
||||||
|
setQueryToProcess={setQueryToProcess}
|
||||||
|
setUploadedFiles={setUploadedFiles}
|
||||||
|
isMobileWidth={isMobileWidth}
|
||||||
|
onConversationIdChange={handleConversationIdChange}
|
||||||
|
conversationId={conversationId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/interface/web/components/ui/tooltip.tsx
Normal file
30
src/interface/web/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
9216
src/interface/web/package-lock.json
generated
Normal file
9216
src/interface/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,19 +35,16 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@types/katex": "^0.16.7",
|
"@radix-ui/themes": "^3.1.1",
|
||||||
"@types/markdown-it": "^14.1.1",
|
|
||||||
"@types/react": "^18",
|
|
||||||
"@types/react-dom": "^18",
|
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"cronstrue": "^2.50.0",
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.3",
|
"eslint-config-next": "14.2.3",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cronstrue": "^2.50.0",
|
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
"lucide-react": "^0.397.0",
|
"lucide-react": "^0.397.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
@@ -60,17 +57,12 @@
|
|||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"shadcn-ui": "^0.8.0",
|
"shadcn-ui": "^0.8.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.3.0",
|
|
||||||
"tailwindcss": "^3.4.4",
|
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
|
||||||
"@types/react-dom": "^18",
|
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.3",
|
"eslint-config-next": "14.2.3",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@@ -79,7 +71,15 @@
|
|||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
"nodemon": "^3.1.3",
|
"nodemon": "^3.1.3",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"typescript": "^5"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"tailwindcss": "^3.4.6",
|
||||||
|
"typescript": "^5",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
|
"@types/markdown-it": "^14.1.1",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"tabWidth": 4
|
"tabWidth": 4
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern: /to-(blue|yellow|green|pink|purple)-(50|100|200|950)/,
|
||||||
|
variants: ['dark'],
|
||||||
|
},
|
||||||
|
],
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
'./pages/**/*.{ts,tsx}',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user