mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00: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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user