mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 05:39:12 +00:00
Modularize code and implemenet share experience
This commit is contained in:
@@ -21,11 +21,6 @@ div.inputBox {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.actualInputArea {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr auto auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.inputBox {
|
input.inputBox {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from './chat.module.css';
|
import styles from './chat.module.css';
|
||||||
import React, { Suspense, useEffect, useRef, useState } from 'react';
|
import React, { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import SuggestionCard from '../components/suggestions/suggestionCard';
|
import SuggestionCard from '../components/suggestions/suggestionCard';
|
||||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||||
@@ -10,289 +10,16 @@ import NavMenu from '../components/navMenu/navMenu';
|
|||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import Loading from '../components/loading/loading';
|
import Loading from '../components/loading/loading';
|
||||||
|
|
||||||
import { handleCompiledReferences, handleImageResponse, setupWebSocket, uploadDataForIndexing } from '../common/chatFunctions';
|
import { handleCompiledReferences, handleImageResponse, setupWebSocket } from '../common/chatFunctions';
|
||||||
import { Progress } from "@/components/ui/progress"
|
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import {
|
|
||||||
ArrowCircleUp,
|
|
||||||
ArrowRight,
|
|
||||||
Browser,
|
|
||||||
ChatsTeardrop,
|
|
||||||
FileArrowUp,
|
|
||||||
GlobeSimple,
|
|
||||||
Gps,
|
|
||||||
Image,
|
|
||||||
Microphone,
|
|
||||||
Notebook,
|
|
||||||
Question,
|
|
||||||
Robot,
|
|
||||||
Shapes
|
|
||||||
} from '@phosphor-icons/react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandSeparator,
|
|
||||||
CommandShortcut,
|
|
||||||
} from "@/components/ui/command"
|
|
||||||
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { StreamMessage } from '../components/chatMessage/chatMessage';
|
import { StreamMessage } from '../components/chatMessage/chatMessage';
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
|
||||||
import { Popover, PopoverContent } from '@/components/ui/popover';
|
|
||||||
import { PopoverTrigger } from '@radix-ui/react-popover';
|
|
||||||
import { welcomeConsole } from '../common/utils';
|
import { welcomeConsole } from '../common/utils';
|
||||||
|
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
||||||
|
import { useAuthenticatedData } from '../common/auth';
|
||||||
|
|
||||||
interface ChatInputProps {
|
|
||||||
sendMessage: (message: string) => void;
|
|
||||||
sendDisabled: boolean;
|
|
||||||
setUploadedFiles?: (files: string[]) => void;
|
|
||||||
conversationId?: string | null;
|
|
||||||
chatOptionsData?: ChatOptions | null;
|
|
||||||
isMobileWidth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChatInputArea(props: ChatInputProps) {
|
|
||||||
const [message, setMessage] = useState('');
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [warning, setWarning] = useState<string | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
|
|
||||||
const [progressValue, setProgressValue] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!uploading) {
|
|
||||||
setProgressValue(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploading) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setProgressValue((prev) => {
|
|
||||||
const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5
|
|
||||||
const nextValue = prev + increment;
|
|
||||||
return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100
|
|
||||||
});
|
|
||||||
}, 800);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [uploading]);
|
|
||||||
|
|
||||||
function onSendMessage() {
|
|
||||||
props.sendMessage(message);
|
|
||||||
setMessage('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSlashCommandClick(command: string) {
|
|
||||||
setMessage(`/${command} `);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileButtonClick() {
|
|
||||||
if (!fileInputRef.current) return;
|
|
||||||
fileInputRef.current.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (!event.target.files) return;
|
|
||||||
|
|
||||||
uploadDataForIndexing(
|
|
||||||
event.target.files,
|
|
||||||
setWarning,
|
|
||||||
setUploading,
|
|
||||||
setError,
|
|
||||||
props.setUploadedFiles,
|
|
||||||
props.conversationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconForSlashCommand(command: string) {
|
|
||||||
if (command.includes('summarize')) {
|
|
||||||
return <Gps className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('help')) {
|
|
||||||
return <Question className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('automation')) {
|
|
||||||
return <Robot className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('webpage')) {
|
|
||||||
return <Browser className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('notes')) {
|
|
||||||
return <Notebook className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('image')) {
|
|
||||||
return <Image className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('default')) {
|
|
||||||
return <Shapes className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('general')) {
|
|
||||||
return <ChatsTeardrop className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('online')) {
|
|
||||||
return <GlobeSimple className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
return <ArrowRight className='h-4 w-4 mr-2' />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
uploading && (
|
|
||||||
<AlertDialog
|
|
||||||
open={uploading}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
<Progress
|
|
||||||
indicatorColor='bg-slate-500'
|
|
||||||
className='w-full h-2 rounded-full'
|
|
||||||
value={progressValue} />
|
|
||||||
</AlertDialogDescription>
|
|
||||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setUploading(false)}>Dismiss</AlertDialogAction>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)}
|
|
||||||
{
|
|
||||||
warning && (
|
|
||||||
<AlertDialog
|
|
||||||
open={warning !== null}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogDescription>{warning}</AlertDialogDescription>
|
|
||||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setWarning(null)}>Close</AlertDialogAction>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
error && (
|
|
||||||
<AlertDialog
|
|
||||||
open={error !== null}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Oh no!</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>Something went wrong while uploading your data</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogDescription>{error}</AlertDialogDescription>
|
|
||||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setError(null)}>Close</AlertDialogAction>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
(message.startsWith('/') && message.split(' ').length === 1) &&
|
|
||||||
<div className='flex justify-center text-center'>
|
|
||||||
<Popover
|
|
||||||
open={message.startsWith('/')}>
|
|
||||||
<PopoverTrigger className='flex justify-center text-center'>
|
|
||||||
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
||||||
className={`${props.isMobileWidth ? 'w-[100vw]' : 'w-full'} rounded-md`}>
|
|
||||||
<Command className='max-w-full'>
|
|
||||||
<CommandInput placeholder="Type a command or search..." value={message} className='hidden' />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No matching commands.</CommandEmpty>
|
|
||||||
<CommandGroup heading="Agent Tools">
|
|
||||||
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
|
||||||
<CommandItem
|
|
||||||
key={key}
|
|
||||||
className={`text-md`}
|
|
||||||
onSelect={() => handleSlashCommandClick(key)}>
|
|
||||||
<div
|
|
||||||
className='grid grid-cols-1 gap-1'>
|
|
||||||
<div
|
|
||||||
className='font-bold flex items-center'>
|
|
||||||
{getIconForSlashCommand(key)}
|
|
||||||
/{key}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
<CommandSeparator />
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className={`${styles.actualInputArea} flex items-center justify-between`}>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
multiple={true}
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant={'ghost'}
|
|
||||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
|
||||||
disabled={props.sendDisabled}
|
|
||||||
onClick={handleFileButtonClick}>
|
|
||||||
<FileArrowUp weight='fill' />
|
|
||||||
</Button>
|
|
||||||
<div className="grid w-full gap-1.5 relative">
|
|
||||||
<Textarea
|
|
||||||
className='border-none min-h-[20px]'
|
|
||||||
placeholder="Type / to see a list of commands"
|
|
||||||
id="message"
|
|
||||||
value={message}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
onSendMessage();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
disabled={props.sendDisabled} />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant={'ghost'}
|
|
||||||
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
|
||||||
disabled={props.sendDisabled}>
|
|
||||||
<Microphone weight='fill' />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
|
|
||||||
onClick={onSendMessage}
|
|
||||||
disabled={props.sendDisabled}>
|
|
||||||
<ArrowCircleUp />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ChatOptions {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
||||||
|
|
||||||
interface ChatBodyDataProps {
|
interface ChatBodyDataProps {
|
||||||
@@ -303,6 +30,7 @@ interface ChatBodyDataProps {
|
|||||||
streamedMessages: StreamMessage[];
|
streamedMessages: StreamMessage[];
|
||||||
setUploadedFiles: (files: string[]) => void;
|
setUploadedFiles: (files: string[]) => void;
|
||||||
isMobileWidth?: boolean;
|
isMobileWidth?: boolean;
|
||||||
|
isLoggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -363,6 +91,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
</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`}>
|
||||||
<ChatInputArea
|
<ChatInputArea
|
||||||
|
isLoggedIn={props.isLoggedIn}
|
||||||
sendMessage={(message) => setMessage(message)}
|
sendMessage={(message) => setMessage(message)}
|
||||||
sendDisabled={processingMessage}
|
sendDisabled={processingMessage}
|
||||||
chatOptionsData={props.chatOptionsData}
|
chatOptionsData={props.chatOptionsData}
|
||||||
@@ -386,6 +115,7 @@ export default function Chat() {
|
|||||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
|
const authenticatedData = useAuthenticatedData();
|
||||||
|
|
||||||
welcomeConsole();
|
welcomeConsole();
|
||||||
|
|
||||||
@@ -492,7 +222,6 @@ export default function Chat() {
|
|||||||
rawQuery: queryToProcess || "",
|
rawQuery: queryToProcess || "",
|
||||||
}
|
}
|
||||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||||
console.log("Messages", messages);
|
|
||||||
setProcessQuerySignal(true);
|
setProcessQuerySignal(true);
|
||||||
} else {
|
} else {
|
||||||
if (!chatWS) {
|
if (!chatWS) {
|
||||||
@@ -537,13 +266,17 @@ export default function Chat() {
|
|||||||
{title}
|
{title}
|
||||||
</title>
|
</title>
|
||||||
<div className={styles.sidePanel}>
|
<div className={styles.sidePanel}>
|
||||||
<SidePanel webSocketConnected={chatWS !== null} conversationId={conversationId} uploadedFiles={uploadedFiles} />
|
<SidePanel
|
||||||
|
webSocketConnected={chatWS !== null}
|
||||||
|
conversationId={conversationId}
|
||||||
|
uploadedFiles={uploadedFiles} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.chatBox}>
|
<div className={styles.chatBox}>
|
||||||
<NavMenu selected="Chat" title={title} />
|
<NavMenu selected="Chat" title={title} />
|
||||||
<div className={styles.chatBoxBody}>
|
<div className={styles.chatBoxBody}>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<ChatBodyData
|
<ChatBodyData
|
||||||
|
isLoggedIn={authenticatedData !== null}
|
||||||
streamedMessages={messages}
|
streamedMessages={messages}
|
||||||
chatOptionsData={chatOptionsData}
|
chatOptionsData={chatOptionsData}
|
||||||
setTitle={setTitle}
|
setTitle={setTitle}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ async function sendChatStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setupWebSocket = async (conversationId: string) => {
|
export const setupWebSocket = async (conversationId: string, initialMessage?: string) => {
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|
||||||
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110';
|
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110';
|
||||||
@@ -86,6 +86,9 @@ export const setupWebSocket = async (conversationId: string) => {
|
|||||||
|
|
||||||
chatWS.onopen = () => {
|
chatWS.onopen = () => {
|
||||||
console.log('WebSocket connection established');
|
console.log('WebSocket connection established');
|
||||||
|
if (initialMessage) {
|
||||||
|
chatWS.send(initialMessage);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
chatWS.onmessage = (event) => {
|
chatWS.onmessage = (event) => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface ChatHistoryProps {
|
|||||||
setTitle: (title: string) => void;
|
setTitle: (title: string) => void;
|
||||||
incomingMessages?: StreamMessage[];
|
incomingMessages?: StreamMessage[];
|
||||||
pendingMessage?: string;
|
pendingMessage?: string;
|
||||||
|
publicConversationSlug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +52,6 @@ function constructTrainOfThought(trainOfThought: string[], lastMessage: boolean,
|
|||||||
|
|
||||||
export default function ChatHistory(props: ChatHistoryProps) {
|
export default function ChatHistory(props: ChatHistoryProps) {
|
||||||
const [data, setData] = useState<ChatHistoryData | null>(null);
|
const [data, setData] = useState<ChatHistoryData | null>(null);
|
||||||
const [isLoading, setLoading] = useState(true);
|
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
const [hasMoreMessages, setHasMoreMessages] = useState(true);
|
const [hasMoreMessages, setHasMoreMessages] = useState(true);
|
||||||
|
|
||||||
@@ -117,38 +117,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
setData(null);
|
setData(null);
|
||||||
}, [props.conversationId]);
|
}, [props.conversationId]);
|
||||||
|
|
||||||
const fetchMoreMessages = (currentPage: number) => {
|
|
||||||
if (!hasMoreMessages || fetchingData) return;
|
|
||||||
const nextPage = currentPage + 1;
|
|
||||||
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then((chatData: ChatResponse) => {
|
|
||||||
props.setTitle(chatData.response.slug);
|
|
||||||
if (chatData && chatData.response && chatData.response.chat.length > 0) {
|
|
||||||
|
|
||||||
if (chatData.response.chat.length === data?.chat.length) {
|
|
||||||
setHasMoreMessages(false);
|
|
||||||
setFetchingData(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(chatData.response);
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
if (currentPage < 2) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
setFetchingData(false);
|
|
||||||
} else {
|
|
||||||
setHasMoreMessages(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(props.incomingMessages);
|
||||||
if (props.incomingMessages) {
|
if (props.incomingMessages) {
|
||||||
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
|
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
|
||||||
if (lastMessage && !lastMessage.completed) {
|
if (lastMessage && !lastMessage.completed) {
|
||||||
@@ -162,25 +132,6 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
|
|
||||||
}, [props.incomingMessages]);
|
}, [props.incomingMessages]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (chatHistoryRef.current) {
|
|
||||||
chatHistoryRef.current.scrollIntoView(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserAtBottom = () => {
|
|
||||||
if (!chatHistoryRef.current) return false;
|
|
||||||
|
|
||||||
// NOTE: This isn't working. It always seems to return true. This is because
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = chatHistoryRef.current as HTMLDivElement;
|
|
||||||
const threshold = 25; // pixels from the bottom
|
|
||||||
|
|
||||||
// Considered at the bottom if within threshold pixels from the bottom
|
|
||||||
return scrollTop + clientHeight >= scrollHeight - threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new MutationObserver((mutationsList, observer) => {
|
const observer = new MutationObserver((mutationsList, observer) => {
|
||||||
// If the addedNodes property has one or more nodes
|
// If the addedNodes property has one or more nodes
|
||||||
@@ -207,9 +158,64 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// if (isLoading) {
|
const fetchMoreMessages = (currentPage: number) => {
|
||||||
// return <Loading />;
|
if (!hasMoreMessages || fetchingData) return;
|
||||||
// }
|
const nextPage = currentPage + 1;
|
||||||
|
|
||||||
|
let conversationFetchURL = '';
|
||||||
|
|
||||||
|
if (props.conversationId) {
|
||||||
|
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`;
|
||||||
|
} else if (props.publicConversationSlug) {
|
||||||
|
conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(conversationFetchURL)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((chatData: ChatResponse) => {
|
||||||
|
props.setTitle(chatData.response.slug);
|
||||||
|
if (chatData && chatData.response && chatData.response.chat.length > 0) {
|
||||||
|
|
||||||
|
if (chatData.response.chat.length === data?.chat.length) {
|
||||||
|
setHasMoreMessages(false);
|
||||||
|
setFetchingData(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(chatData.response);
|
||||||
|
|
||||||
|
if (currentPage < 2) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
setFetchingData(false);
|
||||||
|
} else {
|
||||||
|
setHasMoreMessages(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (chatHistoryRef.current) {
|
||||||
|
chatHistoryRef.current.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserAtBottom = () => {
|
||||||
|
if (!chatHistoryRef.current) return false;
|
||||||
|
|
||||||
|
// NOTE: This isn't working. It always seems to return true. This is because
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = chatHistoryRef.current as HTMLDivElement;
|
||||||
|
const threshold = 25; // pixels from the bottom
|
||||||
|
|
||||||
|
// Considered at the bottom if within threshold pixels from the bottom
|
||||||
|
return scrollTop + clientHeight >= scrollHeight - threshold;
|
||||||
|
}
|
||||||
|
|
||||||
function constructAgentLink() {
|
function constructAgentLink() {
|
||||||
if (!data || !data.agent || !data.agent.slug) return `/agents`;
|
if (!data || !data.agent || !data.agent.slug) return `/agents`;
|
||||||
@@ -226,6 +232,11 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
return data.agent.name;
|
return data.agent.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!props.conversationId && !props.publicConversationSlug) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className={`h-[80vh]`}>
|
<ScrollArea className={`h-[80vh]`}>
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
div.actualInputArea {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto;
|
||||||
|
}
|
||||||
291
src/interface/web/app/components/chatInputArea/chatInputArea.tsx
Normal file
291
src/interface/web/app/components/chatInputArea/chatInputArea.tsx
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
|
||||||
|
import styles from './chatInputArea.module.css';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { uploadDataForIndexing } from '../../common/chatFunctions';
|
||||||
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import {
|
||||||
|
ArrowCircleUp,
|
||||||
|
ArrowRight,
|
||||||
|
Browser,
|
||||||
|
ChatsTeardrop,
|
||||||
|
FileArrowUp,
|
||||||
|
GlobeSimple,
|
||||||
|
Gps,
|
||||||
|
Image,
|
||||||
|
Microphone,
|
||||||
|
Notebook,
|
||||||
|
Question,
|
||||||
|
Robot,
|
||||||
|
Shapes
|
||||||
|
} from '@phosphor-icons/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "@/components/ui/command"
|
||||||
|
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Popover, PopoverContent } from '@/components/ui/popover';
|
||||||
|
import { PopoverTrigger } from '@radix-ui/react-popover';
|
||||||
|
|
||||||
|
export interface ChatOptions {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatInputProps {
|
||||||
|
sendMessage: (message: string) => void;
|
||||||
|
sendDisabled: boolean;
|
||||||
|
setUploadedFiles?: (files: string[]) => void;
|
||||||
|
conversationId?: string | null;
|
||||||
|
chatOptionsData?: ChatOptions | null;
|
||||||
|
isMobileWidth?: boolean;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatInputArea(props: ChatInputProps) {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [warning, setWarning] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
const [progressValue, setProgressValue] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!uploading) {
|
||||||
|
setProgressValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploading) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setProgressValue((prev) => {
|
||||||
|
const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5
|
||||||
|
const nextValue = prev + increment;
|
||||||
|
return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [uploading]);
|
||||||
|
|
||||||
|
function onSendMessage() {
|
||||||
|
props.sendMessage(message);
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSlashCommandClick(command: string) {
|
||||||
|
setMessage(`/${command} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileButtonClick() {
|
||||||
|
if (!fileInputRef.current) return;
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
if (!event.target.files) return;
|
||||||
|
|
||||||
|
uploadDataForIndexing(
|
||||||
|
event.target.files,
|
||||||
|
setWarning,
|
||||||
|
setUploading,
|
||||||
|
setError,
|
||||||
|
props.setUploadedFiles,
|
||||||
|
props.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconForSlashCommand(command: string) {
|
||||||
|
if (command.includes('summarize')) {
|
||||||
|
return <Gps className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('help')) {
|
||||||
|
return <Question className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('automation')) {
|
||||||
|
return <Robot className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('webpage')) {
|
||||||
|
return <Browser className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('notes')) {
|
||||||
|
return <Notebook className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('image')) {
|
||||||
|
return <Image className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('default')) {
|
||||||
|
return <Shapes className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('general')) {
|
||||||
|
return <ChatsTeardrop className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.includes('online')) {
|
||||||
|
return <GlobeSimple className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
return <ArrowRight className='h-4 w-4 mr-2' />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
uploading && (
|
||||||
|
<AlertDialog
|
||||||
|
open={uploading}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Progress
|
||||||
|
indicatorColor='bg-slate-500'
|
||||||
|
className='w-full h-2 rounded-full'
|
||||||
|
value={progressValue} />
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setUploading(false)}>Dismiss</AlertDialogAction>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
warning && (
|
||||||
|
<AlertDialog
|
||||||
|
open={warning !== null}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogDescription>{warning}</AlertDialogDescription>
|
||||||
|
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setWarning(null)}>Close</AlertDialogAction>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
error && (
|
||||||
|
<AlertDialog
|
||||||
|
open={error !== null}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Oh no!</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>Something went wrong while uploading your data</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogDescription>{error}</AlertDialogDescription>
|
||||||
|
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setError(null)}>Close</AlertDialogAction>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(message.startsWith('/') && message.split(' ').length === 1) &&
|
||||||
|
<div className='flex justify-center text-center'>
|
||||||
|
<Popover
|
||||||
|
open={message.startsWith('/')}>
|
||||||
|
<PopoverTrigger className='flex justify-center text-center'>
|
||||||
|
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
className={`${props.isMobileWidth ? 'w-[100vw]' : 'w-full'} rounded-md`}>
|
||||||
|
<Command className='max-w-full'>
|
||||||
|
<CommandInput placeholder="Type a command or search..." value={message} className='hidden' />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No matching commands.</CommandEmpty>
|
||||||
|
<CommandGroup heading="Agent Tools">
|
||||||
|
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
||||||
|
<CommandItem
|
||||||
|
key={key}
|
||||||
|
className={`text-md`}
|
||||||
|
onSelect={() => handleSlashCommandClick(key)}>
|
||||||
|
<div
|
||||||
|
className='grid grid-cols-1 gap-1'>
|
||||||
|
<div
|
||||||
|
className='font-bold flex items-center'>
|
||||||
|
{getIconForSlashCommand(key)}
|
||||||
|
/{key}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className={`${styles.actualInputArea} flex items-center justify-between`}>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple={true}
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||||
|
disabled={props.sendDisabled}
|
||||||
|
onClick={handleFileButtonClick}>
|
||||||
|
<FileArrowUp weight='fill' />
|
||||||
|
</Button>
|
||||||
|
<div className="grid w-full gap-1.5 relative">
|
||||||
|
<Textarea
|
||||||
|
className='border-none min-h-[20px]'
|
||||||
|
placeholder="Type / to see a list of commands"
|
||||||
|
id="message"
|
||||||
|
value={message}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
disabled={props.sendDisabled} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||||
|
disabled={props.sendDisabled}>
|
||||||
|
<Microphone weight='fill' />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-orange-300 hover:bg-orange-500 rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1"
|
||||||
|
onClick={onSendMessage}
|
||||||
|
disabled={props.sendDisabled}>
|
||||||
|
<ArrowCircleUp />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -632,7 +632,7 @@ const fetchChatHistory = async (url: string) => {
|
|||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useChatHistoryRecentFetchRequest = (url: string) => {
|
export const useChatSessionsFetchRequest = (url: string) => {
|
||||||
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
|
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -658,13 +658,13 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
|
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||||
|
|
||||||
const { data: chatHistory } = useChatHistoryRecentFetchRequest('/api/chat/sessions');
|
const { data: chatSessions } = useChatSessionsFetchRequest('/api/chat/sessions');
|
||||||
|
|
||||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatHistory) {
|
if (chatSessions) {
|
||||||
setData(chatHistory);
|
setData(chatSessions);
|
||||||
|
|
||||||
const groupedData: GroupedChatHistory = {};
|
const groupedData: GroupedChatHistory = {};
|
||||||
const subsetOrganizedData: GroupedChatHistory = {};
|
const subsetOrganizedData: GroupedChatHistory = {};
|
||||||
@@ -672,7 +672,7 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|
||||||
chatHistory.forEach((chatHistory) => {
|
chatSessions.forEach((chatHistory) => {
|
||||||
const chatDate = new Date(chatHistory.created);
|
const chatDate = new Date(chatHistory.created);
|
||||||
const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime());
|
const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime());
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
@@ -697,7 +697,7 @@ export default function SidePanel(props: SidePanelProps) {
|
|||||||
setSubsetOrganizedData(subsetOrganizedData);
|
setSubsetOrganizedData(subsetOrganizedData);
|
||||||
setOrganizedData(groupedData);
|
setOrganizedData(groupedData);
|
||||||
}
|
}
|
||||||
}, [chatHistory]);
|
}, [chatSessions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
@@ -722,14 +722,11 @@ 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-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<Image src="khoj-logo.svg"
|
<Image src="/khoj-logo.svg"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
/>
|
/>
|
||||||
{/* <button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
|
||||||
{enabled ? <ArrowLeft className="h-4 w-4" /> : <ArrowRight className="h-4 w-4 mx-2" />}
|
|
||||||
</button> */}
|
|
||||||
{
|
{
|
||||||
isMobileWidth ?
|
isMobileWidth ?
|
||||||
<Drawer>
|
<Drawer>
|
||||||
|
|||||||
33
src/interface/web/app/share/chat/[slug]/layout.tsx
Normal file
33
src/interface/web/app/share/chat/[slug]/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Noto_Sans } from "next/font/google";
|
||||||
|
import "../../../globals.css";
|
||||||
|
|
||||||
|
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Khoj AI - Chat",
|
||||||
|
description: "Use this page to view a chat with Khoj AI.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
312
src/interface/web/app/share/chat/[slug]/page.tsx
Normal file
312
src/interface/web/app/share/chat/[slug]/page.tsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import styles from './sharedChat.module.css';
|
||||||
|
import React, { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import SidePanel from '../../../components/sidePanel/chatHistorySidePanel';
|
||||||
|
import ChatHistory from '../../../components/chatHistory/chatHistory';
|
||||||
|
import NavMenu from '../../../components/navMenu/navMenu';
|
||||||
|
import Loading from '../../../components/loading/loading';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
|
import { welcomeConsole } from '../../../common/utils';
|
||||||
|
import { useAuthenticatedData } from '@/app/common/auth';
|
||||||
|
|
||||||
|
import ChatInputArea, { ChatOptions } from '@/app/components/chatInputArea/chatInputArea';
|
||||||
|
import { StreamMessage } from '@/app/components/chatMessage/chatMessage';
|
||||||
|
import { handleCompiledReferences, handleImageResponse, setupWebSocket } from '@/app/common/chatFunctions';
|
||||||
|
|
||||||
|
|
||||||
|
interface ChatBodyDataProps {
|
||||||
|
chatOptionsData: ChatOptions | null;
|
||||||
|
setTitle: (title: string) => void;
|
||||||
|
setUploadedFiles: (files: string[]) => void;
|
||||||
|
isMobileWidth?: boolean;
|
||||||
|
publicConversationSlug: string;
|
||||||
|
streamedMessages: StreamMessage[];
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
conversationId?: string;
|
||||||
|
setQueryToProcess: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ChatBodyData(props: ChatBodyDataProps) {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [processingMessage, setProcessingMessage] = useState(false);
|
||||||
|
|
||||||
|
if (!props.publicConversationSlug && !props.conversationId) {
|
||||||
|
return (
|
||||||
|
<div className={styles.suggestions}>
|
||||||
|
Whoops, nothing to see here!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (message) {
|
||||||
|
setProcessingMessage(true);
|
||||||
|
props.setQueryToProcess(message);
|
||||||
|
}
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Streamed messages", props.streamedMessages);
|
||||||
|
if (props.streamedMessages &&
|
||||||
|
props.streamedMessages.length > 0 &&
|
||||||
|
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||||
|
|
||||||
|
setProcessingMessage(false);
|
||||||
|
} else {
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
}, [props.streamedMessages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={false ? styles.chatBody : styles.chatBodyFull}>
|
||||||
|
<ChatHistory
|
||||||
|
publicConversationSlug={props.publicConversationSlug}
|
||||||
|
conversationId={props.conversationId || ''}
|
||||||
|
setTitle={props.setTitle}
|
||||||
|
pendingMessage={processingMessage ? message : ''}
|
||||||
|
incomingMessages={props.streamedMessages} />
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
|
||||||
|
<ChatInputArea
|
||||||
|
isLoggedIn={props.isLoggedIn}
|
||||||
|
sendMessage={(message) => setMessage(message)}
|
||||||
|
sendDisabled={!props.isLoggedIn || processingMessage}
|
||||||
|
chatOptionsData={props.chatOptionsData}
|
||||||
|
conversationId={props.conversationId}
|
||||||
|
isMobileWidth={props.isMobileWidth}
|
||||||
|
setUploadedFiles={props.setUploadedFiles} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SharedChat({ params }: { params: { slug: string } }) {
|
||||||
|
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||||
|
const [isLoading, setLoading] = useState(true);
|
||||||
|
const [title, setTitle] = useState('Khoj AI - Chat');
|
||||||
|
const [conversationId, setConversationID] = useState<string | undefined>(undefined);
|
||||||
|
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
|
||||||
|
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||||
|
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||||
|
const [processQuerySignal, setProcessQuerySignal] = useState(false);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||||
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
|
const authenticatedData = useAuthenticatedData();
|
||||||
|
|
||||||
|
welcomeConsole();
|
||||||
|
|
||||||
|
const handleWebSocketMessage = (event: MessageEvent) => {
|
||||||
|
let chunk = event.data;
|
||||||
|
|
||||||
|
let currentMessage = messages.find(message => !message.completed);
|
||||||
|
|
||||||
|
if (!currentMessage) {
|
||||||
|
console.error("No current message found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process WebSocket streamed data
|
||||||
|
if (chunk === "start_llm_response") {
|
||||||
|
console.log("Started streaming", new Date());
|
||||||
|
} else if (chunk === "end_llm_response") {
|
||||||
|
currentMessage.completed = true;
|
||||||
|
} else {
|
||||||
|
// Get the current message
|
||||||
|
// Process and update state with the new message
|
||||||
|
if (chunk.includes("application/json")) {
|
||||||
|
chunk = JSON.parse(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = chunk["content-type"];
|
||||||
|
if (contentType === "application/json") {
|
||||||
|
try {
|
||||||
|
if (chunk.image || chunk.detail) {
|
||||||
|
let responseWithReference = handleImageResponse(chunk);
|
||||||
|
console.log("Image response", responseWithReference);
|
||||||
|
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
|
||||||
|
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
|
||||||
|
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
|
||||||
|
} else if (chunk.type == "status") {
|
||||||
|
currentMessage.trainOfThought.push(chunk.message);
|
||||||
|
} else if (chunk.type == "rate_limit") {
|
||||||
|
console.log("Rate limit message", chunk);
|
||||||
|
currentMessage.rawResponse = chunk.message;
|
||||||
|
} else {
|
||||||
|
console.log("any message", chunk);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing message", error);
|
||||||
|
currentMessage.completed = true;
|
||||||
|
} finally {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Update the current message with the new chunk
|
||||||
|
if (chunk && chunk.includes("### compiled references:")) {
|
||||||
|
let responseWithReference = handleCompiledReferences(chunk, "");
|
||||||
|
currentMessage.rawResponse += responseWithReference.response;
|
||||||
|
|
||||||
|
if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response;
|
||||||
|
if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online;
|
||||||
|
if (responseWithReference.context) currentMessage.context = responseWithReference.context;
|
||||||
|
} else {
|
||||||
|
// If the chunk is not a JSON object, just display it as is
|
||||||
|
currentMessage.rawResponse += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Update the state with the new message, currentMessage
|
||||||
|
setMessages([...messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/chat/options')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((data: ChatOptions) => {
|
||||||
|
setLoading(false);
|
||||||
|
// Render chat options, if any
|
||||||
|
if (data) {
|
||||||
|
setChatOptionsData(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsMobileWidth(window.innerWidth < 786);
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
setIsMobileWidth(window.innerWidth < 786);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryToProcess && !conversationId) {
|
||||||
|
fetch(`/api/chat/share/fork?public_conversation_slug=${params.slug}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
setConversationID(data.conversation_id);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (chatWS && queryToProcess) {
|
||||||
|
// Add a new object to the state
|
||||||
|
const newStreamMessage: StreamMessage = {
|
||||||
|
rawResponse: "",
|
||||||
|
trainOfThought: [],
|
||||||
|
context: [],
|
||||||
|
onlineContext: {},
|
||||||
|
completed: false,
|
||||||
|
timestamp: (new Date()).toISOString(),
|
||||||
|
rawQuery: queryToProcess || "",
|
||||||
|
}
|
||||||
|
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||||
|
setProcessQuerySignal(true);
|
||||||
|
} else {
|
||||||
|
if (!chatWS) {
|
||||||
|
console.error("No WebSocket connection available");
|
||||||
|
}
|
||||||
|
if (!queryToProcess) {
|
||||||
|
console.error("No query to process");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [queryToProcess]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (processQuerySignal && chatWS) {
|
||||||
|
setProcessQuerySignal(false);
|
||||||
|
chatWS.onmessage = handleWebSocketMessage;
|
||||||
|
chatWS?.send(queryToProcess);
|
||||||
|
}
|
||||||
|
}, [processQuerySignal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatWS) {
|
||||||
|
chatWS.onmessage = handleWebSocketMessage;
|
||||||
|
}
|
||||||
|
}, [chatWS]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (conversationId) {
|
||||||
|
const newWS = await setupWebSocket(conversationId, queryToProcess);
|
||||||
|
if (!newWS) {
|
||||||
|
console.error("No WebSocket connection available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setChatWS(newWS);
|
||||||
|
|
||||||
|
// Add a new object to the state
|
||||||
|
const newStreamMessage: StreamMessage = {
|
||||||
|
rawResponse: "",
|
||||||
|
trainOfThought: [],
|
||||||
|
context: [],
|
||||||
|
onlineContext: {},
|
||||||
|
completed: false,
|
||||||
|
timestamp: (new Date()).toISOString(),
|
||||||
|
rawQuery: queryToProcess || "",
|
||||||
|
}
|
||||||
|
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [conversationId]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.main} ${authenticatedData ? styles.chatLayout : ''}`}>
|
||||||
|
<title>
|
||||||
|
{title}
|
||||||
|
</title>
|
||||||
|
<div className={styles.sidePanel}>
|
||||||
|
<SidePanel
|
||||||
|
webSocketConnected={!!conversationId ? (chatWS != null) : true}
|
||||||
|
conversationId={conversationId ?? null}
|
||||||
|
uploadedFiles={uploadedFiles} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.chatBox}>
|
||||||
|
<NavMenu selected="Chat" title={title} />
|
||||||
|
<div className={styles.chatBoxBody}>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<ChatBodyData
|
||||||
|
conversationId={conversationId}
|
||||||
|
streamedMessages={messages}
|
||||||
|
setQueryToProcess={setQueryToProcess}
|
||||||
|
isLoggedIn={authenticatedData !== null}
|
||||||
|
publicConversationSlug={params.slug}
|
||||||
|
chatOptionsData={chatOptionsData}
|
||||||
|
setTitle={setTitle}
|
||||||
|
setUploadedFiles={setUploadedFiles}
|
||||||
|
isMobileWidth={isMobileWidth} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
src/interface/web/app/share/chat/[slug]/sharedChat.module.css
Normal file
127
src/interface/web/app/share/chat/[slug]/sharedChat.module.css
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
div.main {
|
||||||
|
height: 100vh;
|
||||||
|
color: hsla(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.inputBox {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 10px var(--box-shadow-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 12px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input.inputBox {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.inputBox:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.inputBox:focus {
|
||||||
|
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.titleBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBoxBody {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentIndicator a {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.agentIndicator {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
div.chatBody {
|
||||||
|
grid-template-columns: 0fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
div.inputBox {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBoxBody {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatLayout {
|
||||||
|
gap: 0;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"out/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"exclude": [
|
||||||
"skipLibCheck": true,
|
"node_modules"
|
||||||
"downlevelIteration": true,
|
]
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
"out/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -803,11 +803,14 @@ class ConversationAdapters:
|
|||||||
def create_conversation_from_public_conversation(
|
def create_conversation_from_public_conversation(
|
||||||
user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
|
user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
|
||||||
):
|
):
|
||||||
|
scrubbed_title = public_conversation.title if public_conversation.title else public_conversation.slug
|
||||||
|
if scrubbed_title:
|
||||||
|
scrubbed_title = scrubbed_title.replace("-", " ")
|
||||||
return Conversation.objects.create(
|
return Conversation.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
conversation_log=public_conversation.conversation_log,
|
conversation_log=public_conversation.conversation_log,
|
||||||
client=client_app,
|
client=client_app,
|
||||||
slug=public_conversation.slug,
|
slug=scrubbed_title,
|
||||||
title=public_conversation.title,
|
title=public_conversation.title,
|
||||||
agent=public_conversation.agent,
|
agent=public_conversation.agent,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -269,10 +269,15 @@ def get_shared_chat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
meta_log = conversation.conversation_log
|
meta_log = conversation.conversation_log
|
||||||
|
scrubbed_title = conversation.title if conversation.title else conversation.slug
|
||||||
|
|
||||||
|
if scrubbed_title:
|
||||||
|
scrubbed_title = scrubbed_title.replace("-", " ")
|
||||||
|
|
||||||
meta_log.update(
|
meta_log.update(
|
||||||
{
|
{
|
||||||
"conversation_id": conversation.id,
|
"conversation_id": conversation.id,
|
||||||
"slug": conversation.title if conversation.title else conversation.slug,
|
"slug": scrubbed_title,
|
||||||
"agent": agent_metadata,
|
"agent": agent_metadata,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -288,7 +293,7 @@ def get_shared_chat(
|
|||||||
update_telemetry_state(
|
update_telemetry_state(
|
||||||
request=request,
|
request=request,
|
||||||
telemetry_type="api",
|
telemetry_type="api",
|
||||||
api="public_conversation_history",
|
api="chat_history",
|
||||||
**common.__dict__,
|
**common.__dict__,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,7 +335,7 @@ def fork_public_conversation(
|
|||||||
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
||||||
|
|
||||||
# Duplicate Public Conversation to User's Private Conversation
|
# Duplicate Public Conversation to User's Private Conversation
|
||||||
ConversationAdapters.create_conversation_from_public_conversation(
|
new_conversation = ConversationAdapters.create_conversation_from_public_conversation(
|
||||||
user, public_conversation, request.user.client_app
|
user, public_conversation, request.user.client_app
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -346,7 +351,16 @@ def fork_public_conversation(
|
|||||||
|
|
||||||
redirect_uri = str(request.app.url_path_for("chat_page"))
|
redirect_uri = str(request.app.url_path_for("chat_page"))
|
||||||
|
|
||||||
return Response(status_code=200, content=json.dumps({"status": "ok", "next_url": redirect_uri}))
|
return Response(
|
||||||
|
status_code=200,
|
||||||
|
content=json.dumps(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"next_url": redirect_uri,
|
||||||
|
"conversation_id": new_conversation.id,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_chat.post("/share")
|
@api_chat.post("/share")
|
||||||
@@ -451,7 +465,6 @@ async def create_chat_session(
|
|||||||
|
|
||||||
|
|
||||||
@api_chat.get("/options", response_class=Response)
|
@api_chat.get("/options", response_class=Response)
|
||||||
@requires(["authenticated"])
|
|
||||||
async def chat_options(
|
async def chat_options(
|
||||||
request: Request,
|
request: Request,
|
||||||
common: CommonQueryParams,
|
common: CommonQueryParams,
|
||||||
|
|||||||
Reference in New Issue
Block a user