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:
sabaimran 2024-06-27 06:15:38 -07:00 committed by GitHub
parent 3b7a9358c3
commit 870d9ecdbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3294 additions and 223 deletions

View file

@ -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;
}

View file

@ -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}>

View file

@ -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);
}
})

View file

@ -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);
}
})

View file

@ -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;
}

View file

@ -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;
}

View 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>
);
};

View file

@ -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;

View file

@ -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;

View 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>
);
}

View 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);
}
}

View file

@ -0,0 +1,10 @@
.factCheckerLayout {
max-width: 70vw;
margin: auto;
}
@media screen and (max-width: 700px) {
.factCheckerLayout {
max-width: 90vw;
}
}

View 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>
);
}

View 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>
)
}

View file

@ -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;
--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;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--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)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
--primary: #f9f5de;
--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;
--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;
}
.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;
}
}
* {
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;
@layer base {
* {
@apply border-border;
}
} */
body {
@apply bg-background text-foreground;
}
h1 {
@apply text-4xl font-bold;
}
body {
font-family: var(--font-family);
}
pre code.hljs {
white-space: preserve;
}
a {
text-decoration: underline;
}
}

View file

@ -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>
);

View 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"
}
}

View 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,
}

View 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 }

View 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 }

View 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,
}

View 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 }

View 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 }

View 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))
}

View file

@ -31,7 +31,16 @@ const nextConfig = {
protocol: "https",
hostname: "assets.khoj.dev",
},
] : undefined,
] : [
{
protocol: "https",
hostname: "*"
},
{
protocol: "http",
hostname: "*"
}
]
}
};

View file

@ -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",

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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

View file

@ -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):

View 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,
},
),
]

View file

@ -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)

View file

@ -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):

View file

@ -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))

View file

@ -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)