mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Add a fact checker feature with updated styling (#835)
- Add an experimental feature used for fact-checking falsifiable statements with customizable models. See attached screenshot for example. Once you input a statement that needs to be fact-checked, Khoj goes on a research spree to verify or refute it. - Integrate frontend libraries for [Tailwind](https://tailwindcss.com/) and [ShadCN](https://ui.shadcn.com/) for easier UI development. Update corresponding styling for some existing UI components. - Add component for model selection - Add backend support for sharing arbitrary packets of data that will be consumed by specific front-end views in shareable scenarios
This commit is contained in:
parent
3b7a9358c3
commit
870d9ecdbf
35 changed files with 3294 additions and 223 deletions
|
@ -1,7 +1,6 @@
|
|||
div.titleBar {
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
font-size: larger;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agentPersonality p {
|
||||
|
@ -33,7 +32,6 @@ div.agent img {
|
|||
|
||||
div.agent a {
|
||||
text-decoration: none;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
div#agentsHeader {
|
||||
|
@ -56,23 +54,20 @@ div.agentInfo button {
|
|||
padding: 4px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--summer-sun);
|
||||
font: inherit;
|
||||
color: var(--main-text-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div#agentsHeader a:hover,
|
||||
div.agentInfo button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
}
|
||||
|
||||
div.agent {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 20px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
|
@ -81,9 +76,6 @@ div.agent {
|
|||
}
|
||||
|
||||
div.agentModal {
|
||||
padding: 20px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(18.48deg,rgba(252, 213, 87, 0.25) 2.76%,rgba(197, 0, 0, 0) 17.23%),linear-gradient(200.6deg,rgba(244, 229, 68, 0.25) 4.13%,rgba(230, 26, 26, 0) 20.54%);
|
||||
}
|
||||
|
||||
|
@ -129,16 +121,17 @@ svg.newConvoButton {
|
|||
}
|
||||
|
||||
div.agentModalContainer {
|
||||
position: absolute;
|
||||
position: fixed; /* Changed from absolute to fixed */
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
height: 100%; /* This ensures it covers the viewport height */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(1,1,1,0.5);
|
||||
z-index: 1000; /* Ensure it's above other content */
|
||||
overflow-y: auto; /* Allows scrolling within the modal if needed */
|
||||
}
|
||||
|
||||
div.agentModal {
|
||||
|
@ -161,26 +154,28 @@ div.agentModalActions button {
|
|||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: var(--summer-sun);
|
||||
color: var(--main-text-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
div.agentModalActions button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(--primary-hover);
|
||||
box-shadow: 0 0 10px var(hsla(--background));
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
div.agentList {
|
||||
width: 90%;
|
||||
padding: 0;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
div.agentModal {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
}
|
||||
.loader {
|
||||
width: 48px;
|
||||
|
@ -201,7 +196,6 @@ div.agentModalActions button:hover {
|
|||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border-left: 4px solid var(--summer-sun);
|
||||
border-bottom: 4px solid transparent;
|
||||
animation: rotation 0.5s linear infinite reverse;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import useSWR from 'swr';
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useAuthenticatedData, UserProfile } from '../common/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
||||
export interface AgentData {
|
||||
|
@ -27,7 +28,6 @@ async function openChat(slug: string, userData: UserProfile | null) {
|
|||
}
|
||||
|
||||
const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" });
|
||||
// const data = await response.json();
|
||||
if (response.status == 200) {
|
||||
window.location.href = `/chat`;
|
||||
} else if(response.status == 403 || response.status == 401) {
|
||||
|
@ -74,7 +74,7 @@ function AgentModal(props: AgentModalProps) {
|
|||
<h2>{props.data.name}</h2>
|
||||
</div>
|
||||
<div className={styles.agentModalActions}>
|
||||
<button onClick={() => {
|
||||
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
|
||||
navigator.clipboard.writeText(`${window.location.host}/agents?agent=${props.data.slug}`);
|
||||
setCopiedToClipboard(true);
|
||||
}}>
|
||||
|
@ -91,21 +91,23 @@ function AgentModal(props: AgentModalProps) {
|
|||
width={24}
|
||||
height={24} />
|
||||
}
|
||||
</button>
|
||||
<button onClick={() => props.setShowModal(false)}>
|
||||
</Button>
|
||||
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
|
||||
props.setShowModal(false);
|
||||
}}>
|
||||
<Image
|
||||
src="Close.svg"
|
||||
alt="Close"
|
||||
width={24}
|
||||
height={24} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{props.data.personality}</p>
|
||||
<div className={styles.agentInfo}>
|
||||
<button onClick={() => openChat(props.data.slug, props.userData)}>
|
||||
<Button className='bg-yellow-400 hover:bg-yellow-500' onClick={() => openChat(props.data.slug, props.userData)}>
|
||||
Chat
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -140,19 +142,23 @@ function AgentCard(props: AgentCardProps) {
|
|||
</div>
|
||||
</Link>
|
||||
<div className={styles.agentInfo}>
|
||||
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
||||
<button className={styles.infoButton} onClick={() => {
|
||||
setShowModal(true);
|
||||
} }>
|
||||
<h2>{props.data.name}</h2>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.agentInfo}>
|
||||
<button onClick={() => openChat(props.data.slug, userData)}>
|
||||
<Button
|
||||
className='bg-yellow-400 hover:bg-yellow-500'
|
||||
onClick={() => openChat(props.data.slug, userData)}>
|
||||
<Image
|
||||
src="send.svg"
|
||||
alt="Chat"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.agentPersonality}>
|
||||
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
|
||||
|
@ -170,7 +176,7 @@ export default function Agents() {
|
|||
if (error) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
|
@ -183,7 +189,7 @@ export default function Agents() {
|
|||
if (!data) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
|
@ -195,7 +201,7 @@ export default function Agents() {
|
|||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.titleBar}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Talk to a Specialized Agent
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
|
|
|
@ -58,6 +58,7 @@ function Loading() {
|
|||
|
||||
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(target.value);
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
|
@ -71,6 +72,7 @@ export default function Chat() {
|
|||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
console.log(data);
|
||||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -40,6 +40,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
if (chatData) {
|
||||
console.log(chatData);
|
||||
setData(chatData.response);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
div.chatMessageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid black;
|
||||
/* max-width: 80%; */
|
||||
}
|
||||
|
||||
div.you {
|
||||
|
@ -81,5 +76,5 @@ button.codeCopyButton:hover {
|
|||
|
||||
div.feedbackButtons img,
|
||||
button.copyButton img {
|
||||
width: auto;
|
||||
width: 24px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
select.modelPicker {
|
||||
font-size: small;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border: none;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5rem;
|
||||
justify-content: space-between;
|
||||
border-radius: calc(0.5rem - 2px);
|
||||
}
|
||||
|
||||
select.modelPicker:after {
|
||||
grid-area: select;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
div.modelPicker {
|
||||
margin-top: 8px;
|
||||
}
|
152
src/interface/web/app/components/modelPicker/modelPicker.tsx
Normal file
152
src/interface/web/app/components/modelPicker/modelPicker.tsx
Normal file
|
@ -0,0 +1,152 @@
|
|||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
import React, { useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
|
||||
import styles from './modelPicker.module.css';
|
||||
|
||||
export interface Model {
|
||||
id: number;
|
||||
chat_model: string;
|
||||
}
|
||||
|
||||
// Custom fetcher function to fetch options
|
||||
const fetchOptionsRequest = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const useOptionsRequest = (url: string) => {
|
||||
const { data, error } = useSWR<Model[]>(url, fetchOptionsRequest);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchSelectedModel = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const useSelectedModel = (url: string) => {
|
||||
const { data, error } = useSWR<Model>(url, fetchSelectedModel);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
}
|
||||
}
|
||||
|
||||
interface ModelPickerProps {
|
||||
disabled?: boolean;
|
||||
setModelUsed?: (model: Model) => void;
|
||||
initialModel?: Model;
|
||||
}
|
||||
|
||||
export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
|
||||
const { data: models } = useOptionsRequest('/api/config/data/conversation/model/options');
|
||||
const { data: selectedModel } = useSelectedModel('/api/config/data/conversation/model');
|
||||
const [openLoginDialog, setOpenLoginDialog] = React.useState(false);
|
||||
|
||||
let userData = useAuthenticatedData();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.setModelUsed && selectedModel) {
|
||||
props.setModelUsed(selectedModel);
|
||||
}
|
||||
}, [selectedModel]);
|
||||
|
||||
if (!models) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function onSelect(model: Model) {
|
||||
if (!userData) {
|
||||
setOpenLoginDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.setModelUsed) {
|
||||
props.setModelUsed(model);
|
||||
}
|
||||
|
||||
fetch('/api/config/data/conversation/model' + '?id=' + String(model.id), { method: 'POST', body: JSON.stringify(model) })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to select model');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to select model', error);
|
||||
});
|
||||
}
|
||||
|
||||
function isSelected(model: Model) {
|
||||
if (props.initialModel) {
|
||||
return model.id === props.initialModel.id;
|
||||
}
|
||||
return selectedModel?.id === model.id;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.modelPicker}>
|
||||
<select className={styles.modelPicker} onChange={(e) => {
|
||||
const selectedModelId = Number(e.target.value);
|
||||
const selectedModel = models.find((model) => model.id === selectedModelId);
|
||||
if (selectedModel) {
|
||||
onSelect(selectedModel);
|
||||
} else {
|
||||
console.error('Selected model not found', e.target.value);
|
||||
}
|
||||
}} disabled={props.disabled}>
|
||||
{models?.map((model) => (
|
||||
<option key={model.id} value={model.id} selected={isSelected(model)}>
|
||||
{model.chat_model}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<AlertDialog open={openLoginDialog} onOpenChange={setOpenLoginDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>You must be logged in to configure your model.</AlertDialogTitle>
|
||||
<AlertDialogDescription>Once you create an account with Khoj, you can configure your model and use a whole suite of other features. Check out our <a href="https://docs.khoj.dev/">documentation</a> to learn more.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
window.location.href = window.location.origin + '/login';
|
||||
}}>
|
||||
Sign in
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
menu.menu a {
|
||||
color: var(--main-text-color);
|
||||
text-decoration: none;
|
||||
font-size: medium;
|
||||
font-weight: normal;
|
||||
|
@ -51,7 +50,6 @@ div.settingsMenuProfile img {
|
|||
}
|
||||
|
||||
div.settingsMenu {
|
||||
color: var(--main-text-color);
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
|
|
@ -2,7 +2,6 @@ div.panel {
|
|||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--calm-blue);
|
||||
color: var(--main-text-color);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
max-width: auto;
|
||||
|
|
63
src/interface/web/app/components/shareLink/shareLink.tsx
Normal file
63
src/interface/web/app/components/shareLink/shareLink.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface ShareLinkProps {
|
||||
buttonTitle: string;
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
onShare: () => void;
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
const clipboard = navigator.clipboard;
|
||||
if (!clipboard) {
|
||||
return;
|
||||
}
|
||||
clipboard.writeText(text);
|
||||
}
|
||||
|
||||
export default function ShareLink(props: ShareLinkProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
||||
onClick={props.onShare}>
|
||||
{props.buttonTitle}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{props.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="grid flex-1 gap-2">
|
||||
<Label htmlFor="link" className="sr-only">
|
||||
Link
|
||||
</Label>
|
||||
<Input
|
||||
id="link"
|
||||
defaultValue={props.url}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" size="sm" className="px-3" onClick={() => copyToClipboard(props.url)}>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
161
src/interface/web/app/factchecker/factChecker.module.css
Normal file
161
src/interface/web/app/factchecker/factChecker.module.css
Normal file
|
@ -0,0 +1,161 @@
|
|||
input.factVerification {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
margin: auto;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
input.factVerification:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
div.responseText {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
div.response {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
a.titleLink {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a.subLinks {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-weight: small;
|
||||
border-radius: 4px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
div.subLinks {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
div.reference {
|
||||
padding: 12px;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
footer.footer {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.reportActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
button.factCheckButton {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
margin: 8px;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
button.factCheckButton:hover {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
div.spinner {
|
||||
margin: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
-webkit-animation: sk-rotate 2.0s infinite linear;
|
||||
animation: sk-rotate 2.0s infinite linear;
|
||||
}
|
||||
|
||||
div.inputFields {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
/* Loading Animation */
|
||||
div.dot1,
|
||||
div.dot2 {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-radius: 100%;
|
||||
|
||||
-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
}
|
||||
|
||||
div.dot2 {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-rotate {
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
-webkit-transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
-webkit-transform: scale(0.0)
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sk-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.factCheckerLayout {
|
||||
max-width: 70vw;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.factCheckerLayout {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
25
src/interface/web/app/factchecker/layout.tsx
Normal file
25
src/interface/web/app/factchecker/layout.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
import type { Metadata } from "next";
|
||||
import NavMenu from '../components/navMenu/navMenu';
|
||||
import styles from './factCheckerLayout.module.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Fact Checker",
|
||||
description: "Use the Fact Checker with Khoj AI for verifying statements. It can research the internet for you, either refuting or confirming the statement using fresh data.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className={styles.factCheckerLayout}>
|
||||
<NavMenu selected="none" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
576
src/interface/web/app/factchecker/page.tsx
Normal file
576
src/interface/web/app/factchecker/page.tsx
Normal file
|
@ -0,0 +1,576 @@
|
|||
'use client'
|
||||
|
||||
import styles from './factChecker.module.css';
|
||||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import ChatMessage, { Context, OnlineContextData, WebPage } from '../components/chatMessage/chatMessage';
|
||||
import { ModelPicker, Model } from '../components/modelPicker/modelPicker';
|
||||
import ShareLink from '../components/shareLink/shareLink';
|
||||
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import Link from 'next/link';
|
||||
|
||||
|
||||
|
||||
const chatURL = "/api/chat";
|
||||
const verificationPrecursor = "Limit your search to reputable sources. Search the internet for relevant supporting or refuting information. Do not reference my notes. Refuse to answer any queries that are not falsifiable by informing me that you will not answer the question. You're not permitted to ask follow-up questions, so do the best with what you have. Respond with **TRUE** or **FALSE** or **INCONCLUSIVE**, then provide your justification. Fact Check:"
|
||||
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.loadingVerification}>
|
||||
Researching...
|
||||
<div className={styles.spinner}>
|
||||
<div className={`${styles.dot1} bg-blue-300`}></div>
|
||||
<div className={`${styles.dot2} bg-blue-300`}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SupplementReferences {
|
||||
additionalLink: string;
|
||||
response: string;
|
||||
linkTitle: string;
|
||||
}
|
||||
|
||||
|
||||
interface ResponseWithReferences {
|
||||
context?: Context[];
|
||||
online?: {
|
||||
[key: string]: OnlineContextData
|
||||
}
|
||||
response?: string;
|
||||
}
|
||||
|
||||
function handleCompiledReferences(chunk: string, currentResponse: string) {
|
||||
const rawReference = chunk.split("### compiled references:")[1];
|
||||
const rawResponse = chunk.split("### compiled references:")[0];
|
||||
let references: ResponseWithReferences = {};
|
||||
|
||||
// Set the initial response
|
||||
references.response = currentResponse + rawResponse;
|
||||
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
if (rawReferenceAsJson instanceof Array) {
|
||||
references.context = rawReferenceAsJson;
|
||||
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||
references.online = rawReferenceAsJson;
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
|
||||
async function verifyStatement(
|
||||
message: string,
|
||||
conversationId: string,
|
||||
setIsLoading: (loading: boolean) => void,
|
||||
setInitialResponse: (response: string) => void,
|
||||
setInitialReferences: (references: ResponseWithReferences) => void) {
|
||||
setIsLoading(true);
|
||||
// Send a message to the chat server to verify the fact
|
||||
let verificationMessage = `${verificationPrecursor} ${message}`;
|
||||
const apiURL = `${chatURL}?q=${encodeURIComponent(verificationMessage)}&client=web&stream=true&conversation_id=${conversationId}`;
|
||||
try {
|
||||
const response = await fetch(apiURL);
|
||||
if (!response.body) throw new Error("No response body found");
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
let decoder = new TextDecoder();
|
||||
let result = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
let chunk = decoder.decode(value, { stream: true });
|
||||
|
||||
if (chunk.includes("### compiled references:")) {
|
||||
const references = handleCompiledReferences(chunk, result);
|
||||
if (references.response) {
|
||||
result = references.response;
|
||||
setInitialResponse(references.response);
|
||||
setInitialReferences(references);
|
||||
}
|
||||
} else {
|
||||
result += chunk;
|
||||
setInitialResponse(result);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error verifying statement: ", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function spawnNewConversation(setConversationID: (conversationID: string) => void) {
|
||||
|
||||
let createURL = `/api/chat/sessions?client=web`;
|
||||
|
||||
const response = await fetch(createURL, { method: "POST" });
|
||||
|
||||
const data = await response.json();
|
||||
setConversationID(data.conversation_id);
|
||||
}
|
||||
|
||||
|
||||
interface ReferenceVerificationProps {
|
||||
message: string;
|
||||
additionalLink: string;
|
||||
conversationId: string;
|
||||
linkTitle: string;
|
||||
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
|
||||
prefilledResponse?: string;
|
||||
}
|
||||
|
||||
function ReferenceVerification(props: ReferenceVerificationProps) {
|
||||
const [initialResponse, setInitialResponse] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const verificationStatement = `${props.message}. Use this link for reference: ${props.additionalLink}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (props.prefilledResponse) {
|
||||
setInitialResponse(props.prefilledResponse);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => {});
|
||||
}
|
||||
|
||||
}, [verificationStatement, props.conversationId, props.prefilledResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialResponse === "") return;
|
||||
if (props.prefilledResponse) return;
|
||||
|
||||
if (!isLoading) {
|
||||
// Only set the child references when it's done loading and if the initial response is not prefilled (i.e. it was fetched from the server)
|
||||
props.setChildReferencesCallback(props.additionalLink, initialResponse, props.linkTitle);
|
||||
}
|
||||
}, [initialResponse, isLoading, props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading &&
|
||||
<LoadingSpinner />
|
||||
}
|
||||
<ChatMessage chatMessage={
|
||||
{
|
||||
automationId: "",
|
||||
by: "AI",
|
||||
intent: {},
|
||||
message: initialResponse,
|
||||
context: [],
|
||||
created: (new Date()).toISOString(),
|
||||
onlineContext: {}
|
||||
}
|
||||
} setReferencePanelData={() => {}} setShowReferencePanel={() => {}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SupplementalReferenceProps {
|
||||
onlineData?: OnlineContextData;
|
||||
officialFactToVerify: string;
|
||||
conversationId: string;
|
||||
additionalLink: string;
|
||||
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
|
||||
prefilledResponse?: string;
|
||||
linkTitle?: string;
|
||||
}
|
||||
|
||||
function SupplementalReference(props: SupplementalReferenceProps) {
|
||||
const linkTitle = props.linkTitle || props.onlineData?.organic?.[0]?.title || "Reference";
|
||||
const linkAsWebpage = { link: props.additionalLink } as WebPage;
|
||||
return (
|
||||
<Card className={`mt-2 mb-4`}>
|
||||
<CardHeader>
|
||||
<a className={styles.titleLink} href={props.additionalLink} target="_blank" rel="noreferrer">
|
||||
{linkTitle}
|
||||
</a>
|
||||
<WebPageLink {...linkAsWebpage} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ReferenceVerification
|
||||
additionalLink={props.additionalLink}
|
||||
message={props.officialFactToVerify}
|
||||
linkTitle={linkTitle}
|
||||
conversationId={props.conversationId}
|
||||
setChildReferencesCallback={props.setChildReferencesCallback}
|
||||
prefilledResponse={props.prefilledResponse} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const WebPageLink = (webpage: WebPage) => {
|
||||
const webpageDomain = new URL(webpage.link).hostname;
|
||||
return (
|
||||
<div className={styles.subLinks}>
|
||||
<a className={`${styles.subLinks} bg-blue-200 px-2`} href={webpage.link} target="_blank" rel="noreferrer">
|
||||
{webpageDomain}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default function FactChecker() {
|
||||
const [factToVerify, setFactToVerify] = useState("");
|
||||
const [officialFactToVerify, setOfficialFactToVerify] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [initialResponse, setInitialResponse] = useState("");
|
||||
const [clickedVerify, setClickedVerify] = useState(false);
|
||||
const [initialReferences, setInitialReferences] = useState<ResponseWithReferences>();
|
||||
const [childReferences, setChildReferences] = useState<SupplementReferences[]>();
|
||||
const [modelUsed, setModelUsed] = useState<Model>();
|
||||
|
||||
const [conversationID, setConversationID] = useState("");
|
||||
const [runId, setRunId] = useState("");
|
||||
const [loadedFromStorage, setLoadedFromStorage] = useState(false);
|
||||
|
||||
const [initialModel, setInitialModel] = useState<Model>();
|
||||
|
||||
function setChildReferencesCallback(additionalLink: string, response: string, linkTitle: string) {
|
||||
const newReferences = childReferences || [];
|
||||
const exists = newReferences.find((reference) => reference.additionalLink === additionalLink);
|
||||
if (exists) return;
|
||||
newReferences.push({ additionalLink, response, linkTitle });
|
||||
setChildReferences(newReferences);
|
||||
}
|
||||
|
||||
let userData = useAuthenticatedData();
|
||||
|
||||
function storeData() {
|
||||
const data = {
|
||||
factToVerify,
|
||||
response: initialResponse,
|
||||
references: initialReferences,
|
||||
childReferences,
|
||||
runId,
|
||||
modelUsed,
|
||||
};
|
||||
|
||||
fetch(`/api/chat/store/factchecker`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"runId": runId,
|
||||
"storeData": data
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (factToVerify) {
|
||||
document.title = `AI Fact Check: ${factToVerify}`;
|
||||
} else {
|
||||
document.title = 'AI Fact Checker';
|
||||
}
|
||||
}, [factToVerify]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedFact = localStorage.getItem('factToVerify');
|
||||
if (storedFact) {
|
||||
setFactToVerify(storedFact);
|
||||
}
|
||||
|
||||
// Get query params from the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const factToVerifyParam = urlParams.get('factToVerify');
|
||||
|
||||
if (factToVerifyParam) {
|
||||
setFactToVerify(factToVerifyParam);
|
||||
}
|
||||
|
||||
const runIdParam = urlParams.get('runId');
|
||||
if (runIdParam) {
|
||||
setRunId(runIdParam);
|
||||
|
||||
// Define an async function to fetch data
|
||||
const fetchData = async () => {
|
||||
const storedDataURL = `/api/chat/store/factchecker?runId=${runIdParam}`;
|
||||
try {
|
||||
const response = await fetch(storedDataURL);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to fetch stored data");
|
||||
}
|
||||
const storedData = JSON.parse(await response.json());
|
||||
if (storedData) {
|
||||
setOfficialFactToVerify(storedData.factToVerify);
|
||||
setInitialResponse(storedData.response);
|
||||
setInitialReferences(storedData.references);
|
||||
setChildReferences(storedData.childReferences);
|
||||
setInitialModel(storedData.modelUsed);
|
||||
}
|
||||
setLoadedFromStorage(true);
|
||||
} catch (error) {
|
||||
console.error("Error fetching stored data: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Call the async function
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
function onClickVerify() {
|
||||
if (clickedVerify) return;
|
||||
|
||||
// Perform validation checks on the fact to verify
|
||||
if (!factToVerify) {
|
||||
alert("Please enter a fact to verify.");
|
||||
return;
|
||||
}
|
||||
|
||||
setClickedVerify(true);
|
||||
if (!userData) {
|
||||
let currentURL = window.location.href;
|
||||
window.location.href = `/login?next=${currentURL}`;
|
||||
}
|
||||
|
||||
setInitialReferences(undefined);
|
||||
setInitialResponse("");
|
||||
|
||||
spawnNewConversation(setConversationID);
|
||||
|
||||
// Set the runId to a random 12-digit alphanumeric string
|
||||
const newRunId = [...Array(16)].map(() => Math.random().toString(36)[2]).join('');
|
||||
setRunId(newRunId);
|
||||
window.history.pushState({}, document.title, window.location.pathname + `?runId=${newRunId}`);
|
||||
|
||||
setOfficialFactToVerify(factToVerify);
|
||||
setClickedVerify(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationID) return;
|
||||
verifyStatement(officialFactToVerify, conversationID, setIsLoading, setInitialResponse, setInitialReferences);
|
||||
}, [conversationID, officialFactToVerify]);
|
||||
|
||||
// Store factToVerify in localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('factToVerify', factToVerify);
|
||||
}, [factToVerify]);
|
||||
|
||||
// Update the meta tags for the description and og:description
|
||||
useEffect(() => {
|
||||
let metaTag = document.querySelector('meta[name="description"]');
|
||||
if (metaTag) {
|
||||
metaTag.setAttribute('content', initialResponse);
|
||||
}
|
||||
let metaOgTag = document.querySelector('meta[property="og:description"]');
|
||||
if (!metaOgTag) {
|
||||
metaOgTag = document.createElement('meta');
|
||||
metaOgTag.setAttribute('property', 'og:description');
|
||||
document.getElementsByTagName('head')[0].appendChild(metaOgTag);
|
||||
}
|
||||
metaOgTag.setAttribute('content', initialResponse);
|
||||
}, [initialResponse]);
|
||||
|
||||
const renderReferences = (conversationId: string, initialReferences: ResponseWithReferences, officialFactToVerify: string, loadedFromStorage: boolean, childReferences?: SupplementReferences[]) => {
|
||||
if (loadedFromStorage && childReferences) {
|
||||
return renderSupplementalReferences(childReferences);
|
||||
}
|
||||
|
||||
const seenLinks = new Set();
|
||||
|
||||
// Any links that are present in webpages should not be searched again
|
||||
Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
|
||||
const webpages = onlineData?.webpages || [];
|
||||
// Webpage can be a list or a single object
|
||||
if (webpages instanceof Array) {
|
||||
for (let i = 0; i < webpages.length; i++) {
|
||||
const webpage = webpages[i];
|
||||
const additionalLink = webpage.link || '';
|
||||
if (seenLinks.has(additionalLink)) {
|
||||
return null;
|
||||
}
|
||||
seenLinks.add(additionalLink);
|
||||
}
|
||||
} else {
|
||||
let singleWebpage = webpages as WebPage;
|
||||
const additionalLink = singleWebpage.link || '';
|
||||
if (seenLinks.has(additionalLink)) {
|
||||
return null;
|
||||
}
|
||||
seenLinks.add(additionalLink);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
|
||||
let additionalLink = '';
|
||||
|
||||
// Loop through organic links until we find one that hasn't been searched
|
||||
for (let i = 0; i < onlineData?.organic?.length; i++) {
|
||||
const webpage = onlineData?.organic?.[i];
|
||||
additionalLink = webpage.link || '';
|
||||
|
||||
if (!seenLinks.has(additionalLink)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
seenLinks.add(additionalLink);
|
||||
|
||||
if (additionalLink === '') return null;
|
||||
|
||||
return (
|
||||
<SupplementalReference
|
||||
key={index}
|
||||
onlineData={onlineData}
|
||||
officialFactToVerify={officialFactToVerify}
|
||||
conversationId={conversationId}
|
||||
additionalLink={additionalLink}
|
||||
setChildReferencesCallback={setChildReferencesCallback} />
|
||||
);
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
const renderSupplementalReferences = (references: SupplementReferences[]) => {
|
||||
return references.map((reference, index) => {
|
||||
return (
|
||||
<SupplementalReference
|
||||
key={index}
|
||||
additionalLink={reference.additionalLink}
|
||||
officialFactToVerify={officialFactToVerify}
|
||||
conversationId={conversationID}
|
||||
linkTitle={reference.linkTitle}
|
||||
setChildReferencesCallback={setChildReferencesCallback}
|
||||
prefilledResponse={reference.response} />
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
const renderWebpages = (webpages: WebPage[] | WebPage) => {
|
||||
if (webpages instanceof Array) {
|
||||
return webpages.map((webpage, index) => {
|
||||
return WebPageLink(webpage);
|
||||
});
|
||||
} else {
|
||||
return WebPageLink(webpages);
|
||||
}
|
||||
};
|
||||
|
||||
function constructShareUrl() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('runId', runId);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.factCheckerContainer}>
|
||||
<h1 className={`${styles.response} font-large outline-slate-800 dark:outline-slate-200`}>
|
||||
AI Fact Checker
|
||||
</h1>
|
||||
<footer className={`${styles.footer} mt-4`}>
|
||||
This is an experimental AI tool. It may make mistakes.
|
||||
</footer>
|
||||
{
|
||||
initialResponse && initialReferences && childReferences
|
||||
?
|
||||
<div className={styles.reportActions}>
|
||||
<Button asChild variant='secondary'>
|
||||
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
|
||||
Try Another
|
||||
</Link>
|
||||
</Button>
|
||||
<ShareLink
|
||||
buttonTitle='Share report'
|
||||
title="AI Fact Checking Report"
|
||||
description="Share this fact checking report with others. Anyone who has this link will be able to view the report."
|
||||
url={constructShareUrl()}
|
||||
onShare={loadedFromStorage ? () => {} : storeData} />
|
||||
</div>
|
||||
: <div className={styles.newReportActions}>
|
||||
<div className={`${styles.inputFields} mt-4`}>
|
||||
<Input
|
||||
type="text"
|
||||
maxLength={200}
|
||||
placeholder="Enter a falsifiable statement to verify"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setFactToVerify(e.target.value)}
|
||||
value={factToVerify}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
onClickVerify();
|
||||
}
|
||||
}}
|
||||
onFocus={(e) => e.target.placeholder = ""}
|
||||
onBlur={(e) => e.target.placeholder = "Enter a falsifiable statement to verify"} />
|
||||
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>Verify</Button>
|
||||
</div>
|
||||
<h3 className={`mt-4 mb-4`}>
|
||||
Try with a particular model. You must be <a href="/config" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
<ModelPicker disabled={isLoading || loadedFromStorage} setModelUsed={setModelUsed} initialModel={initialModel} />
|
||||
{isLoading && <div className={styles.loading}>
|
||||
<LoadingSpinner />
|
||||
</div>}
|
||||
{
|
||||
initialResponse &&
|
||||
<Card className={`mt-4`}>
|
||||
<CardHeader>
|
||||
<CardTitle>{officialFactToVerify}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.responseText}>
|
||||
<ChatMessage chatMessage={
|
||||
{
|
||||
automationId: "",
|
||||
by: "AI",
|
||||
intent: {},
|
||||
message: initialResponse,
|
||||
context: [],
|
||||
created: (new Date()).toISOString(),
|
||||
onlineContext: {}
|
||||
}
|
||||
} setReferencePanelData={() => {}} setShowReferencePanel={() => {}} />
|
||||
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{
|
||||
initialReferences && initialReferences.online && Object.keys(initialReferences.online).length > 0 && (
|
||||
<div className={styles.subLinks}>
|
||||
{
|
||||
Object.entries(initialReferences.online).map(([key, onlineData], index) => {
|
||||
const webpages = onlineData?.webpages || [];
|
||||
return renderWebpages(webpages);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
}
|
||||
{
|
||||
initialReferences &&
|
||||
<div className={styles.referenceContainer}>
|
||||
<h2 className="mt-4 mb-4">Supplements</h2>
|
||||
<div className={styles.references}>
|
||||
{ initialReferences.online !== undefined &&
|
||||
renderReferences(conversationID, initialReferences, officialFactToVerify, loadedFromStorage, childReferences)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,158 +1,80 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 224 71.4% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 224 71.4% 4.1%;
|
||||
--primary: 209.1 100% 40.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 14.3% 95.9%;
|
||||
--secondary-foreground: 220.9 39.3% 11%;
|
||||
--muted: 220 14.3% 95.9%;
|
||||
--muted-foreground: 220 8.9% 46.1%;
|
||||
--accent: 220 14.3% 95.9%;
|
||||
--accent-foreground: 220.9 39.3% 11%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 13% 91%;
|
||||
--input: 220 13% 91%;
|
||||
--ring: 209.1 100% 40.8%;
|
||||
--radius: 0.5rem;
|
||||
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
||||
--primary-hover: #fee285;
|
||||
}
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
|
||||
--primary: #f9f5de;
|
||||
--primary-hover: #fee285;
|
||||
--primary-focus: rgba(255, 179, 0, 0.125);
|
||||
--primary-inverse: rgba(0, 0, 0, 0.75);
|
||||
--background-color: #f5f4f3;
|
||||
--frosted-background-color: rgba(245, 244, 243, 0.75);
|
||||
--main-text-color: #475569;
|
||||
--summer-sun: #fcc50b;
|
||||
--water: #44b9da;
|
||||
--leaf: #7b990a;
|
||||
--flower: #ffaeae;
|
||||
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
||||
--calm-blue: #E4D9D9;
|
||||
--calmer-blue: #e4cdcd;
|
||||
--calm-green: #d0f5d6;
|
||||
--intense-green: #1C2841;
|
||||
.dark {
|
||||
--background: 224 71.4% 4.1%;
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 224 71.4% 4.1%;
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 224 71.4% 4.1%;
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 215 27.9% 16.9%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 215 27.9% 16.9%;
|
||||
--muted-foreground: 217.9 10.6% 64.9%;
|
||||
--accent: 215 27.9% 16.9%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 215 27.9% 16.9%;
|
||||
--input: 215 27.9% 16.9%;
|
||||
--ring: 263.4 70% 50.4%;
|
||||
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
h1 {
|
||||
@apply text-4xl font-bold;
|
||||
}
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
--primary: #f9f5de;
|
||||
--primary-hover: #fee285;
|
||||
--primary-focus: rgba(255, 179, 0, 0.125);
|
||||
--primary-inverse: rgba(0, 0, 0, 0.75);
|
||||
--background-color: #f5f4f3;
|
||||
--frosted-background-color: rgba(245, 244, 243, 0.75);
|
||||
--main-text-color: #475569;
|
||||
--summer-sun: #fcc50b;
|
||||
--water: #44b9da;
|
||||
--leaf: #7b990a;
|
||||
--flower: #ffaeae;
|
||||
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
|
||||
}
|
||||
pre code.hljs {
|
||||
white-space: preserve;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
input,
|
||||
div,
|
||||
button,
|
||||
p,
|
||||
textarea {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--main-text-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
white-space: preserve;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Uncomment when ready for dark-mode */
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
} */
|
||||
|
|
|
@ -2,7 +2,7 @@ import styles from "./page.module.css";
|
|||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<main className={`${styles.main} text-3xl font-bold underline`}>
|
||||
Hi, Khoj here.
|
||||
</main>
|
||||
);
|
||||
|
|
17
src/interface/web/components.json
Normal file
17
src/interface/web/components.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
141
src/interface/web/components/ui/alert-dialog.tsx
Normal file
141
src/interface/web/components/ui/alert-dialog.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
56
src/interface/web/components/ui/button.tsx
Normal file
56
src/interface/web/components/ui/button.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
79
src/interface/web/components/ui/card.tsx
Normal file
79
src/interface/web/components/ui/card.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
122
src/interface/web/components/ui/dialog.tsx
Normal file
122
src/interface/web/components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
25
src/interface/web/components/ui/input.tsx
Normal file
25
src/interface/web/components/ui/input.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
26
src/interface/web/components/ui/label.tsx
Normal file
26
src/interface/web/components/ui/label.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
6
src/interface/web/lib/utils.ts
Normal file
6
src/interface/web/lib/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
|
@ -31,7 +31,16 @@ const nextConfig = {
|
|||
protocol: "https",
|
||||
hostname: "assets.khoj.dev",
|
||||
},
|
||||
] : undefined,
|
||||
] : [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "*"
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -17,15 +17,28 @@
|
|||
"windowsexport": "yarn build && xcopy out ..\\..\\khoj\\interface\\built /E /Y && yarn windowscollectstatic"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.10",
|
||||
"lucide-react": "^0.397.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-highlightjs": "^4.1.0",
|
||||
"next": "14.2.3",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"swr": "^2.2.5"
|
||||
"shadcn-ui": "^0.8.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
|
6
src/interface/web/postcss.config.js
Normal file
6
src/interface/web/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
80
src/interface/web/tailwind.config.ts
Normal file
80
src/interface/web/tailwind.config.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import type { Config } from "tailwindcss"
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config
|
||||
|
||||
export default config
|
File diff suppressed because it is too large
Load diff
|
@ -27,6 +27,7 @@ from khoj.database.models import (
|
|||
ChatModelOptions,
|
||||
ClientApplication,
|
||||
Conversation,
|
||||
DataStore,
|
||||
Entry,
|
||||
FileObject,
|
||||
GithubConfig,
|
||||
|
@ -599,6 +600,19 @@ class PublicConversationAdapters:
|
|||
return f"/share/chat/{public_conversation.slug}/"
|
||||
|
||||
|
||||
class DataStoreAdapters:
|
||||
@staticmethod
|
||||
async def astore_data(data: dict, key: str, user: KhojUser, private: bool = True):
|
||||
if await DataStore.objects.filter(key=key).aexists():
|
||||
return key
|
||||
await DataStore.objects.acreate(value=data, key=key, owner=user, private=private)
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
async def aretrieve_public_data(key: str):
|
||||
return await DataStore.objects.filter(key=key, private=False).afirst()
|
||||
|
||||
|
||||
class ConversationAdapters:
|
||||
@staticmethod
|
||||
def make_public_conversation_copy(conversation: Conversation):
|
||||
|
|
38
src/khoj/database/migrations/0049_datastore.py
Normal file
38
src/khoj/database/migrations/0049_datastore.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 4.2.11 on 2024-06-26 17:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0048_voicemodeloption_uservoicemodelconfig"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DataStore",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("key", models.CharField(max_length=200, unique=True)),
|
||||
("value", models.JSONField(default=dict)),
|
||||
("private", models.BooleanField(default=False)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -359,3 +359,10 @@ class EntryDates(BaseModel):
|
|||
class UserRequests(BaseModel):
|
||||
user = models.ForeignKey(KhojUser, on_delete=models.CASCADE)
|
||||
slug = models.CharField(max_length=200)
|
||||
|
||||
|
||||
class DataStore(BaseModel):
|
||||
key = models.CharField(max_length=200, unique=True)
|
||||
value = models.JSONField(default=dict)
|
||||
private = models.BooleanField(default=False)
|
||||
owner = models.ForeignKey(KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True)
|
||||
|
|
|
@ -2,7 +2,7 @@ import json
|
|||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import unquote
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
|
@ -15,6 +15,7 @@ from websockets import ConnectionClosedOK
|
|||
|
||||
from khoj.database.adapters import (
|
||||
ConversationAdapters,
|
||||
DataStoreAdapters,
|
||||
EntryAdapters,
|
||||
FileObjectAdapters,
|
||||
PublicConversationAdapters,
|
||||
|
@ -94,6 +95,53 @@ def get_file_filter(request: Request, conversation_id: str) -> Response:
|
|||
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
class FactCheckerStoreDataFormat(BaseModel):
|
||||
factToVerify: str
|
||||
response: str
|
||||
references: Any
|
||||
childReferences: List[Any]
|
||||
runId: str
|
||||
modelUsed: Dict[str, Any]
|
||||
|
||||
|
||||
class FactCheckerStoreData(BaseModel):
|
||||
runId: str
|
||||
storeData: FactCheckerStoreDataFormat
|
||||
|
||||
|
||||
@api_chat.post("/store/factchecker", response_class=Response)
|
||||
@requires(["authenticated"])
|
||||
async def store_factchecker(request: Request, common: CommonQueryParams, data: FactCheckerStoreData):
|
||||
user = request.user.object
|
||||
|
||||
update_telemetry_state(
|
||||
request=request,
|
||||
telemetry_type="api",
|
||||
api="store_factchecker",
|
||||
**common.__dict__,
|
||||
)
|
||||
fact_checker_key = f"factchecker_{data.runId}"
|
||||
await DataStoreAdapters.astore_data(data.storeData.model_dump_json(), fact_checker_key, user, private=False)
|
||||
return Response(content=json.dumps({"status": "ok"}), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
@api_chat.get("/store/factchecker", response_class=Response)
|
||||
async def get_factchecker(request: Request, common: CommonQueryParams, runId: str):
|
||||
update_telemetry_state(
|
||||
request=request,
|
||||
telemetry_type="api",
|
||||
api="read_factchecker",
|
||||
**common.__dict__,
|
||||
)
|
||||
|
||||
fact_checker_key = f"factchecker_{runId}"
|
||||
|
||||
data = await DataStoreAdapters.aretrieve_public_data(fact_checker_key)
|
||||
if data is None:
|
||||
return Response(status_code=404)
|
||||
return Response(content=json.dumps(data.value), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
@api_chat.post("/conversation/file-filters", response_class=Response)
|
||||
@requires(["authenticated"])
|
||||
def add_file_filter(request: Request, filter: FilterRequest):
|
||||
|
|
|
@ -229,18 +229,44 @@ async def get_all_filenames(
|
|||
return await sync_to_async(list)(EntryAdapters.get_all_filenames_by_source(user, content_source)) # type: ignore[call-arg]
|
||||
|
||||
|
||||
@api_config.post("/data/conversation/model", status_code=200)
|
||||
@api_config.get("/data/conversation/model/options", response_model=Dict[str, Union[str, int]])
|
||||
def get_chat_model_options(
|
||||
request: Request,
|
||||
client: Optional[str] = None,
|
||||
):
|
||||
conversation_options = ConversationAdapters.get_conversation_processor_options().all()
|
||||
|
||||
all_conversation_options = list()
|
||||
for conversation_option in conversation_options:
|
||||
all_conversation_options.append({"chat_model": conversation_option.chat_model, "id": conversation_option.id})
|
||||
|
||||
return Response(content=json.dumps(all_conversation_options), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
@api_config.get("/data/conversation/model")
|
||||
@requires(["authenticated"])
|
||||
def get_user_chat_model(
|
||||
request: Request,
|
||||
client: Optional[str] = None,
|
||||
):
|
||||
user = request.user.object
|
||||
|
||||
chat_model = ConversationAdapters.get_conversation_config(user)
|
||||
|
||||
if chat_model is None:
|
||||
chat_model = ConversationAdapters.get_default_conversation_config()
|
||||
|
||||
return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model}))
|
||||
|
||||
|
||||
@api_config.post("/data/conversation/model", status_code=200)
|
||||
@requires(["authenticated", "premium"])
|
||||
async def update_chat_model(
|
||||
request: Request,
|
||||
id: str,
|
||||
client: Optional[str] = None,
|
||||
):
|
||||
user = request.user.object
|
||||
subscribed = has_required_scope(request, ["premium"])
|
||||
|
||||
if not subscribed:
|
||||
raise HTTPException(status_code=403, detail="User is not subscribed to premium")
|
||||
|
||||
new_config = await ConversationAdapters.aset_user_conversation_processor(user, int(id))
|
||||
|
||||
|
|
|
@ -129,6 +129,16 @@ def experimental_page(request: Request):
|
|||
)
|
||||
|
||||
|
||||
@web_client.get("/factchecker", response_class=FileResponse)
|
||||
def fact_checker_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"factchecker/index.html",
|
||||
context={
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@web_client.get("/login", response_class=FileResponse)
|
||||
def login_page(request: Request):
|
||||
next_url = get_next_url(request)
|
||||
|
|
Loading…
Reference in a new issue