Merge pull request #878 from khoj-ai/features/big-upgrade-chat-ux

Spring UI: Modernize UX for normie development
This commit is contained in:
sabaimran 2024-08-04 21:32:18 -07:00 committed by GitHub
commit 8bc28fb11d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
163 changed files with 15321 additions and 14579 deletions

View file

@ -34,4 +34,4 @@ Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into you
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -27,4 +27,4 @@ LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -31,6 +31,6 @@ Ollama exposes a local [OpenAI API compatible server](https://github.com/ollama/
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.

View file

@ -34,4 +34,4 @@ For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advan
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -23,7 +23,7 @@ Khoj will keep these files in sync to provide contextual responses when you sear
## Setup
1. Install the [Khoj Desktop app](https://khoj.dev/downloads) for your OS
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
3. Set your Khoj API Key on the *Settings* page of the Khoj Desktop app
4. [Optional] Add any files, folders you'd like Khoj to be aware of on the *Settings* page and Click *Save*
These files and folders will be automatically kept in sync for you

View file

@ -30,7 +30,7 @@ sidebar_position: 2
| ![khoj search on emacs](/img/khoj_search_on_emacs.png) | ![khoj chat on emacs](/img/khoj_chat_on_emacs.png) |
## Setup
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
2. Add below snippet to your Emacs config file, usually at `~/.emacs.d/init.el`

View file

@ -23,7 +23,7 @@ sidebar_position: 3
1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel
2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
4. Set your Khoj API Key in the Khoj plugin settings in Obsidian
See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for more details on installing Obsidian plugins.

View file

@ -10,7 +10,7 @@ Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan the QQ code below on
Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode.
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config).
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/settings).
If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud.

View file

@ -4,11 +4,11 @@ The Github integration allows you to index as many repositories as you want. It'
# Configure your settings
1. Go to [https://app.khoj.dev/config](https://app.khoj.dev/config) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
1. Go to [https://app.khoj.dev/settings](https://app.khoj.dev/settings) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
## Use the Github plugin
1. Generate a [classic PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) from [Github](https://github.com/settings/tokens) with `repo` and `admin:org` scopes at least.
2. Navigate to [https://app.khoj.dev/config/content-source/github](https://app.khoj.dev/config/content-source/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
2. Navigate to [https://app.khoj.dev/settings#github](https://app.khoj.dev/settings#github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
3. Click `Save`. Go back to the settings page and click `Configure`.
4. Go to [https://app.khoj.dev/](https://app.khoj.dev/) and start searching!

View file

@ -2,7 +2,7 @@
The Notion integration allows you to search/chat with your Notion workspaces. [Notion](https://notion.so/) is a platform people use for taking notes, especially for collaboration.
Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
Go to https://app.khoj.dev/settings to connect your Notion workspace(s) to Khoj.
![notion_integration](https://assets.khoj.dev/notion_integration.gif)
@ -13,7 +13,7 @@ Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
![setup_new_integration](https://github.com/khoj-ai/khoj/assets/65192171/b056e057-d4dc-47dc-aad3-57b59a22c68b)
3. Share all the workspaces that you want to integrate with the Khoj integration you just made in the previous step
![enable_workspace](https://github.com/khoj-ai/khoj/assets/65192171/98290303-b5b8-4cb0-b32c-f68c6923a3d0)
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/config/content-source/notion. Click `Save`.
5. Click `Configure` in http://localhost:42110/config to index your Notion workspace(s).
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at [http://localhost:42110/settings#notion](http://localhost:42110/settings#notion). Click `Save`.
5. Click `Configure` in http://localhost:42110/settings to index your Notion workspace(s).
That's it! You should be ready to start searching and chatting. Make sure you've configured your [chat settings](/get-started/setup#2-configure).

View file

@ -1,7 +1,7 @@
---
sidebar_position: 0
slug: /
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain ai", "ai search engine"]
keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain", "personal ai", "ai search engine"]
---
# Overview
@ -9,7 +9,7 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"
<p align="center"><img src="/img/khoj-logo-sideways-500.png" width="200" alt="Khoj Logo"></img></p>
<div align="center">
<b>An AI copilot for your Second Brain</b>
<b>Your Second Brain</b>
</div>
<br />

View file

@ -9,7 +9,7 @@ import {themes as prismThemes} from 'prism-react-renderer';
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Khoj AI',
tagline: 'An AI copilot for your Second Brain',
tagline: 'Your Second Brain',
staticDirectories: ['assets'],

View file

@ -3,7 +3,7 @@
"name": "Khoj",
"version": "1.17.0",
"minAppVersion": "0.15.0",
"description": "An AI copilot for your Second Brain",
"description": "Your Second Brain",
"author": "Khoj Inc.",
"authorUrl": "https://github.com/khoj-ai",
"isDesktopOnly": false

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "khoj"
description = "An AI copilot for your Second Brain"
description = "Your Second Brain"
readme = "README.md"
license = "AGPL-3.0-or-later"
requires-python = ">=3.10"

View file

@ -233,11 +233,15 @@ function pushDataToKhoj (regenerate = false) {
// Request indexing files on server. With upto 1000 files in each request
for (let i = 0; i < filesDataToPush.length; i += 1000) {
const syncUrl = `${hostURL}/api/content?client=desktop`;
const filesDataGroup = filesDataToPush.slice(i, i + 1000);
const formData = new FormData();
filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) });
let request = axios.post(`${hostURL}/api/v1/index/update?force=${regenerate}&client=desktop`, formData, { headers });
requests.push(request);
requests.push(
regenerate
? axios.put(syncUrl, formData, { headers })
: axios.patch(syncUrl, formData, { headers })
);
}
// Wait for requests batch to finish
@ -253,7 +257,7 @@ function pushDataToKhoj (regenerate = false) {
console.error(error);
state["completed"] = false;
if (error?.response?.status === 429 && (BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')))) {
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/config">Upgrade your plan</a> to unlock more space.`;
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/settings#subscription">Upgrade your plan</a> to unlock more space.`;
const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config'));
if (win) win.webContents.send('needsSubscription', true);
} else if (error?.code === 'ECONNREFUSED') {

View file

@ -1,8 +1,8 @@
{
"name": "Khoj",
"version": "1.17.0",
"description": "An AI copilot for your Second Brain",
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
"description": "Your Second Brain",
"author": "Khoj Inc. <team@khoj.dev>",
"license": "GPL-3.0-or-later",
"homepage": "https://khoj.dev",
"repository": "\"https://github.com/khoj-ai/khoj\"",

View file

@ -182,7 +182,7 @@ window.updateStateAPI.onUpdateState((event, state) => {
window.needsSubscriptionAPI.onNeedsSubscription((event, needsSubscription) => {
console.log("needs subscription", needsSubscription);
if (needsSubscription) {
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/config");
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/settings#subscription");
needsSubscriptionElement.style.display = 'block';
}
});

View file

@ -212,12 +212,12 @@
const headers = { 'Authorization': `Bearer ${khojToken}` };
// Populate type dropdown field with enabled content types only
fetch(`${hostURL}/api/config/types`, { headers })
fetch(`${hostURL}/api/content/types`, { headers })
.then(response => response.json())
.then(enabled_types => {
// Show warning if no content types are enabled
if (enabled_types.detail) {
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/settings'>settings page</a>.</div>";
document.getElementById("query").setAttribute("disabled", "disabled");
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
return [];

View file

@ -85,7 +85,7 @@ async function populateHeaderPane() {
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> ${username} </div>
<a id="github-nav" class="khoj-nav" href="https://github.com/khoj-ai/khoj">GitHub</a>
<a id="settings-nav" class="khoj-nav" href="./config.html"> Settings</a>
<a id="settings-nav" class="khoj-nav" href="./settings.html"> Settings</a>
</div>
</div>
` : ''}

View file

@ -4,8 +4,8 @@
;; Author: Debanjum Singh Solanky <debanjum@khoj.dev>
;; Saba Imran <saba@khoj.dev>
;; Description: An AI copilot for your Second Brain
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image
;; Description: Your Second Brain
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
;; Version: 1.17.0
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
@ -99,7 +99,7 @@
:type 'boolean)
(defcustom khoj-api-key nil
"API Key to your Khoj. Default at https://app.khoj.dev/config#clients."
"API Key to your Khoj. Default at https://app.khoj.dev/settings#clients."
:group 'khoj
:type 'string)
@ -424,12 +424,12 @@ Auto invokes setup steps on calling main entrypoint."
"Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server.
Append 'TYPE-QUERY' as query parameter in request url.
Specify `BOUNDARY' used to separate files in request header."
(let ((url-request-method "POST")
(let ((url-request-method ((if force) "PUT" "PATCH"))
(url-request-data body)
(url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary))
("Authorization" . ,(format "Bearer %s" khoj-api-key)))))
(with-current-buffer
(url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false"))
(url-retrieve (format "%s/api/content?%s&client=emacs" khoj-server-url type-query)
;; render response from indexing API endpoint on server
(lambda (status)
(if (not (plist-get status :error))
@ -697,7 +697,7 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS."
(defun khoj--get-enabled-content-types ()
"Get content types enabled for search from API."
(khoj--call-api "/api/config/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
(khoj--call-api "/api/content/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
(defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank is-find-similar)
"Query Khoj Search API with QUERY, CONTENT-TYPE and RERANK as query params.

View file

@ -3,7 +3,7 @@
"name": "Khoj",
"version": "1.17.0",
"minAppVersion": "0.15.0",
"description": "An AI copilot for your Second Brain",
"description": "Your Second Brain",
"author": "Khoj Inc.",
"authorUrl": "https://github.com/khoj-ai",
"isDesktopOnly": false

View file

@ -1,7 +1,7 @@
{
"name": "Khoj",
"version": "1.17.0",
"description": "An AI copilot for your Second Brain",
"description": "Your Second Brain",
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
"license": "GPL-3.0-or-later",
"main": "src/main.js",
@ -14,7 +14,8 @@
"search",
"chat",
"AI",
"assistant"
"assistant",
"second brain"
],
"devDependencies": {
"@types/dompurify": "^3.0.5",

View file

@ -92,10 +92,11 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
for (let i = 0; i < fileData.length; i += 1000) {
const filesGroup = fileData.slice(i, i + 1000);
const formData = new FormData();
const method = regenerate ? "PUT" : "PATCH";
filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
// Call Khoj backend to update index with all markdown, pdf files
const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, {
method: 'POST',
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
method: method,
headers: {
'Authorization': `Bearer ${setting.khojApiKey}`,
},
@ -204,12 +205,12 @@ export function getBackendStatusMessage(
): string {
// Welcome message with default settings. Khoj cloud always expects an API key.
if (!khojApiKey && khojUrl === 'https://app.khoj.dev')
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`;
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/settings#clients and set it in the Khoj plugin settings on Obsidian`;
if (!connectedToServer)
return `Could not connect to Khoj at ${khojUrl}. Ensure your can access it`;
else if (!userEmail)
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/config#clients to log in`;
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/settings#clients to log in`;
else if (userEmail === 'default@example.com')
// Logged in as default user in anonymous mode
return `✅ Signed in to Khoj`;

View file

@ -1,3 +1,11 @@
{
"extends": "next/core-web-vitals"
"extends": [
"next",
"next/core-web-vitals",
"plugin:prettier/recommended"
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "warn"
}
}

View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
yarn run lint-staged
yarn test

View file

@ -6,7 +6,7 @@ div.titleBar {
.agentPersonality p {
white-space: inherit;
overflow: hidden;
height: 78px;
height: 77px;
line-height: 1.5;
}
@ -16,27 +16,15 @@ div.agentPersonality {
overflow: hidden;
}
div.agentInfo {
font-size: medium;
div.pageLayout {
max-width: 60vw;
margin: auto;
margin-bottom: 2rem;
}
div.agentInfo a,
div.agentInfo h2 {
margin: 0;
}
div.agent img {
border-radius: 50%;
object-fit: cover;
}
div.agent a {
text-decoration: none;
}
div#agentsHeader {
display: grid;
grid-template-columns: auto;
div.sidePanel {
position: fixed;
height: 100%;
}
button.infoButton {
@ -47,163 +35,33 @@ button.infoButton {
font-size: medium;
}
div#agentsHeader a,
div.agentInfo button {
font-size: 24px;
font-weight: bold;
padding: 4px;
border: none;
border-radius: 8px;
font: inherit;
cursor: pointer;
transition: background-color 0.3s;
}
div#agentsHeader a:hover,
div.agentInfo button:hover {
box-shadow: 0 0 10px var(--primary-hover);
}
div.agent {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 4px;
align-items: center;
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%);
}
div.agentModal {
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%);
}
div.agentModalContent button {
width: 100%;
margin: 10px 0;
padding: 8px;
}
div.agentModalHeader {
display: grid;
grid-template-columns: 1fr auto;
}
div.agentAvatar {
display: flex;
align-items: center;
gap: 8px;
}
div.agentModalContent p {
white-space: break-spaces;
line-height: 1.5;
}
div.agentInfo {
text-align: left;
}
div.agentList {
display: grid;
gap: 20px;
padding: 20px;
padding-top: 30px;
margin-right: auto;
grid-auto-flow: row;
grid-template-columns: 1fr 1fr;
margin-left: auto;
}
svg.newConvoButton {
width: 20px;
margin-left: 5px;
}
div.agentModalContainer {
position: fixed; /* Changed from absolute to fixed */
top: 0;
left: 0;
width: 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 {
position: relative;
width: 50%;
margin: auto;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.agentModalActions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
div.agentModalActions button {
padding: 8px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
div.agentModalActions button:hover {
box-shadow: 0 0 10px var(hsla(--background));
}
@media only screen and (max-width: 700px) {
@media only screen and (max-width: 768px) {
div.agentList {
width: 90%;
width: 100%;
padding: 0;
margin-right: auto;
margin-left: auto;
grid-template-columns: 1fr;
}
div.agentModal {
width: 90%;
div.pageLayout {
max-width: 90vw;
}
}
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
border-top: 4px solid var(--primary-color);
border-right: 4px solid transparent;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
.loader::after {
content: '';
box-sizing: border-box;
position: absolute;
left: 0;
top: 0;
width: 48px;
height: 48px;
border-radius: 50%;
border-bottom: 4px solid transparent;
animation: rotation 0.5s linear infinite reverse;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
div.sidePanel {
position: relative;
height: 100%;
}
}

View file

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

View file

@ -1,13 +1,14 @@
import type { Metadata } from "next";
import NavMenu from '../components/navMenu/navMenu';
import styles from './agentsLayout.module.css';
import { Noto_Sans } from "next/font/google";
import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Agents",
description: "Use Agents with Khoj AI for deeper, more personalized queries.",
description: "Find a specialized agent that can help you address more specific needs.",
icons: {
icon: '/static/favicon.ico',
icon: "/static/favicon.ico",
},
};
@ -17,9 +18,20 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<div className={`${styles.agentsLayout}`}>
<NavMenu selected="Agents" />
{children}
</div>
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -1,26 +1,61 @@
'use client'
"use client";
import styles from './agents.module.css';
import styles from "./agents.module.css";
import Image from 'next/image';
import Link from 'next/link';
import useSWR from 'swr';
import Image from "next/image";
import useSWR from "swr";
import { useEffect, useState } from 'react';
import { useEffect, useState } from "react";
import { useAuthenticatedData, UserProfile } from '../common/auth';
import { Button } from '@/components/ui/button';
import { useAuthenticatedData, UserProfile } from "../common/auth";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import LoginPrompt from "../components/loginPrompt/loginPrompt";
import { InlineLoading } from "../components/loading/loading";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import NavMenu from "../components/navMenu/navMenu";
import { getIconFromIconName } from "../common/iconUtils";
import { convertColorToTextClass } from "../common/colorUtils";
import { Alert, AlertDescription } from "@/components/ui/alert";
export interface AgentData {
slug: string;
avatar: string;
name: string;
personality: string;
persona: string;
color: string;
icon: string;
}
async function openChat(slug: string, userData: UserProfile | null) {
const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`;
if (!userData) {
window.location.href = unauthenticatedRedirectUrl;
@ -28,160 +63,226 @@ 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) {
window.location.href = `/chat?conversationId=${data.conversation_id}`;
} else if (response.status == 403 || response.status == 401) {
window.location.href = unauthenticatedRedirectUrl;
} else {
alert("Failed to start chat session");
}
}
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
interface AgentModalProps {
data: AgentData;
setShowModal: (show: boolean) => void;
userData: UserProfile | null;
}
const agentsFetcher = () =>
window
.fetch("/api/agents")
.then((res) => res.json())
.catch((err) => console.log(err));
interface AgentCardProps {
data: AgentData;
userProfile: UserProfile | null;
}
function AgentModal(props: AgentModalProps) {
const [copiedToClipboard, setCopiedToClipboard] = useState(false);
useEffect(() => {
if (copiedToClipboard) {
setTimeout(() => setCopiedToClipboard(false), 3000);
}
}, [copiedToClipboard]);
return (
<div className={styles.agentModalContainer}>
<div className={styles.agentModal}>
<div className={styles.agentModalContent}>
<div className={styles.agentModalHeader}>
<div className={styles.agentAvatar}>
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
<h2>{props.data.name}</h2>
</div>
<div className={styles.agentModalActions}>
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
navigator.clipboard.writeText(`${window.location.host}/agents?agent=${props.data.slug}`);
setCopiedToClipboard(true);
}}>
{
copiedToClipboard ?
<Image
src="copy-button-success.svg"
alt="Copied"
width={24}
height={24} />
: <Image
src="share.svg"
alt="Copy Link"
width={24}
height={24} />
}
</Button>
<Button className='bg-transparent hover:bg-yellow-500' onClick={() => {
props.setShowModal(false);
}}>
<Image
src="Close.svg"
alt="Close"
width={24}
height={24} />
</Button>
</div>
</div>
<p>{props.data.personality}</p>
<div className={styles.agentInfo}>
<Button className='bg-yellow-400 hover:bg-yellow-500' onClick={() => openChat(props.data.slug, props.userData)}>
Chat
</Button>
</div>
</div>
</div>
</div>
);
isMobileWidth: boolean;
}
function AgentCard(props: AgentCardProps) {
const searchParams = new URLSearchParams(window.location.search);
const agentSlug = searchParams.get('agent');
const agentSlug = searchParams.get("agent");
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const userData = props.userProfile;
if (showModal) {
window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`);
window.history.pushState(
{},
`Khoj AI - Agent ${props.data.slug}`,
`/agents?agent=${props.data.slug}`,
);
}
const stylingString = convertColorToTextClass(props.data.color);
return (
<div className={styles.agent}>
{
showModal && <AgentModal data={props.data} setShowModal={setShowModal} userData={userData} />
}
<Link href={`/agent/${props.data.slug}`}>
<div className={styles.agentAvatar}>
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
<Card
className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}
>
{showLoginPrompt && (
<LoginPrompt
loginRedirectMessage={`Sign in to start chatting with ${props.data.name}`}
onOpenChange={setShowLoginPrompt}
/>
)}
<CardHeader>
<CardTitle>
{!props.isMobileWidth ? (
<Dialog
open={showModal}
onOpenChange={() => {
setShowModal(!showModal);
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}
>
<DialogTrigger>
<div className="flex items-center relative top-2">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
)}
{props.data.name}
</div>
</DialogTrigger>
<div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => openChat(props.data.slug, userData)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
onClick={() => setShowLoginPrompt(true)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
)}
</div>
<DialogContent className="whitespace-pre-line max-h-[80vh]">
<DialogHeader>
<div className="flex items-center">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={32}
height={50}
/>
)}
<p className="font-bold text-lg">{props.data.name}</p>
</div>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
{props.data.persona}
</div>
<DialogFooter>
<Button
className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
onClick={() => {
openChat(props.data.slug, userData);
setShowModal(false);
}}
>
<PaperPlaneTilt
className={`w-6 h-6 m-2 ${convertColorToTextClass(props.data.color)}`}
/>
Start Chatting
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<Drawer
open={showModal}
onOpenChange={(open) => {
setShowModal(open);
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
}}
>
<DrawerTrigger>
<div className="flex items-center">
{getIconFromIconName(props.data.icon, props.data.color) || (
<Image
src={props.data.avatar}
alt={props.data.name}
width={50}
height={50}
/>
)}
{props.data.name}
</div>
</DrawerTrigger>
<div className="float-right">
{props.userProfile ? (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
onClick={() => openChat(props.data.slug, userData)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
) : (
<Button
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
onClick={() => setShowLoginPrompt(true)}
>
<PaperPlaneTilt
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
/>
</Button>
)}
</div>
<DrawerContent className="whitespace-pre-line p-2">
<DrawerHeader>
<DrawerTitle>{props.data.name}</DrawerTitle>
<DrawerDescription>Full Prompt</DrawerDescription>
</DrawerHeader>
{props.data.persona}
<DrawerFooter>
<DrawerClose>Done</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className={styles.agentPersonality}>
<button
className={`${styles.infoButton} text-neutral-500 dark:text-white`}
onClick={() => setShowModal(true)}
>
<p>{props.data.persona}</p>
</button>
</div>
</Link>
<div className={styles.agentInfo}>
<button className={styles.infoButton} onClick={() => {
setShowModal(true);
} }>
<h2>{props.data.name}</h2>
</button>
</div>
<div className={styles.agentInfo}>
<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>
</div>
<div className={styles.agentPersonality}>
<button className={styles.infoButton} onClick={() => setShowModal(true)}>
<p>{props.data.personality}</p>
</button>
</div>
</div>
</CardContent>
</Card>
);
}
export default function Agents() {
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
const userData = useAuthenticatedData();
const { data, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
revalidateOnFocus: false,
});
const authenticatedData = useAuthenticatedData();
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
useEffect(() => {
if (typeof window !== "undefined") {
setIsMobileWidth(window.innerWidth < 768);
}
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 768);
});
}, []);
if (error) {
return (
<main className={styles.main}>
<div className={`${styles.titleBar} text-5xl`}>
Talk to a Specialized Agent
</div>
<div className={styles.agentList}>
Error loading agents
</div>
<div className={`${styles.titleBar} text-5xl`}>Agents</div>
<div className={styles.agentList}>Error loading agents</div>
</main>
);
}
@ -189,25 +290,66 @@ export default function Agents() {
if (!data) {
return (
<main className={styles.main}>
<div className={`${styles.titleBar} text-5xl`}>
Talk to a Specialized Agent
</div>
<div className={styles.agentList}>
Loading agents...
<InlineLoading /> booting up your agents
</div>
</main>
);
}
return (
<main className={styles.main}>
<div className={`${styles.titleBar} text-5xl`}>
Talk to a Specialized Agent
</div>
<div className={styles.agentList}>
{data.map(agent => (
<AgentCard key={agent.slug} data={agent} userProfile={userData} />
))}
<main className={`w-full mx-auto`}>
<div className={`grid w-full mx-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`${styles.pageLayout} w-full`}>
<div className={`pt-6 md:pt-8 flex justify-between`}>
<h1 className="text-3xl flex items-center">Agents</h1>
<div className="ml-auto float-right border p-2 pt-3 rounded-xl font-bold hover:bg-stone-100 dark:hover:bg-neutral-900">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="flex flex-row">
<Plus className="pr-2 w-6 h-6" />
<p className="pr-2">Create Agent</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Coming Soon!</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{showLoginPrompt && (
<LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt}
/>
)}
<Alert className="bg-secondary border-none my-4">
<AlertDescription>
<Lightning weight={"fill"} className="h-4 w-4 text-purple-400 inline" />
<span className="font-bold">How it works</span> Use any of these
specialized personas to tune your conversation to your needs.
</AlertDescription>
</Alert>
<div className={`${styles.agentList}`}>
{data.map((agent) => (
<AgentCard
key={agent.slug}
data={agent}
userProfile={authenticatedData}
isMobileWidth={isMobileWidth}
/>
))}
</div>
</div>
</div>
</main>
);

View file

@ -0,0 +1,36 @@
div.automationsLayout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
div.automationCard {
display: grid;
grid-template-rows: auto 1fr auto;
}
div.pageLayout {
max-width: 60vw;
margin: auto;
margin-bottom: 2rem;
}
div.sidePanel {
position: fixed;
height: 100%;
}
@media screen and (max-width: 768px) {
div.automationsLayout {
grid-template-columns: 1fr;
}
div.pageLayout {
max-width: 90vw;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View file

@ -0,0 +1,25 @@
import type { Metadata } from "next";
import { Toaster } from "@/components/ui/toaster";
import "../globals.css";
export const metadata: Metadata = {
title: "Khoj AI - Automations",
description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.",
icons: {
icon: "/static/favicon.ico",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div>
{children}
<Toaster />
</div>
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,37 @@
div.main {
height: 100vh;
color: black;
height: 100dvh;
color: hsla(var(--foreground));
}
.suggestions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
display: flex;
overflow-x: none;
height: 50%;
padding: 10px;
white-space: nowrap;
/* grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); */
gap: 1rem;
justify-content: center;
/* justify-content: center; */
}
div.inputBox {
display: grid;
grid-template-columns: 1fr auto;
padding: 1rem;
border-radius: 1rem;
background-color: #f5f5f5;
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
margin-bottom: 20px;
gap: 12px;
align-content: center;
}
input.inputBox {
border: none;
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
input.inputBox:focus {
border: none;
outline: none;
background-color: transparent;
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.chatBodyFull {
@ -54,7 +57,7 @@ div.chatBody {
}
.inputBox {
color: black;
color: hsla(var(--foreground));
}
div.chatLayout {
@ -65,9 +68,7 @@ div.chatLayout {
div.chatBox {
display: grid;
gap: 1rem;
height: 100%;
padding: 1rem;
}
div.titleBar {
@ -75,12 +76,45 @@ div.titleBar {
grid-template-columns: 1fr auto;
}
@media (max-width: 768px) {
div.chatBoxBody {
display: grid;
height: 100%;
width: 70%;
margin: auto;
}
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.agentIndicator {
padding: 10px;
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBoxBody {
width: 100%;
}
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
height: 100%;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
}
}

View file

@ -5,20 +5,34 @@ import "../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Chat",
description: "Use this page to chat with Khoj AI.",
title: "Khoj AI - Chat",
description:
"Ask anything. Khoj will use the internet and your docs to answer, paint and even automate stuff for you.",
icons: {
icon: "/static/favicon.ico",
},
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
);
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -1,108 +1,284 @@
'use client'
"use client";
import styles from './chat.module.css';
import React, { Suspense, useEffect, useState } from 'react';
import styles from "./chat.module.css";
import React, { Suspense, useEffect, useState } from "react";
import SuggestionCard from '../components/suggestions/suggestionCard';
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import ChatHistory from '../components/chatHistory/chatHistory';
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
import NavMenu from '../components/navMenu/navMenu';
import { useSearchParams } from 'next/navigation'
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel';
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import ChatHistory from "../components/chatHistory/chatHistory";
import { useSearchParams } from "next/navigation";
import Loading from "../components/loading/loading";
import 'katex/dist/katex.min.css';
import { processMessageChunk } from "../common/chatFunctions";
interface ChatOptions {
[key: string]: string
import "katex/dist/katex.min.css";
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
import { useIPLocationData, welcomeConsole } from "../common/utils";
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
import { useAuthenticatedData } from "../common/auth";
import { AgentData } from "../agents/page";
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
onConversationIdChange?: (conversationId: string) => void;
setQueryToProcess: (query: string) => void;
streamedMessages: StreamMessage[];
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
isLoggedIn: boolean;
}
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
function ChatBodyData(props: ChatBodyDataProps) {
const searchParams = useSearchParams();
const conversationId = searchParams.get("conversationId");
const [message, setMessage] = useState("");
const [processingMessage, setProcessingMessage] = useState(false);
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null }) {
const searchParams = useSearchParams();
const conversationId = searchParams.get('conversationId');
const [showReferencePanel, setShowReferencePanel] = useState(true);
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
const setQueryToProcess = props.setQueryToProcess;
const onConversationIdChange = props.onConversationIdChange;
useEffect(() => {
const storedMessage = localStorage.getItem("message");
if (storedMessage) {
setProcessingMessage(true);
setQueryToProcess(storedMessage);
}
}, [setQueryToProcess]);
useEffect(() => {
if (message) {
setProcessingMessage(true);
setQueryToProcess(message);
}
}, [message, setQueryToProcess]);
useEffect(() => {
if (conversationId) {
onConversationIdChange?.(conversationId);
}
}, [conversationId, onConversationIdChange]);
useEffect(() => {
if (
props.streamedMessages &&
props.streamedMessages.length > 0 &&
props.streamedMessages[props.streamedMessages.length - 1].completed
) {
setProcessingMessage(false);
} else {
setMessage("");
}
}, [props.streamedMessages]);
if (!conversationId) {
return (
<div className={styles.suggestions}>
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
<SuggestionCard
key={key}
title={`/${key}`}
body={value}
link='#' // replace with actual link if available
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
/>
))}
</div>
);
window.location.href = "/";
return;
}
return(
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} />
{
(hasValidReferences(referencePanelData) && showReferencePanel) &&
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
}
</div>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement;
console.log(target.value);
return (
<>
<div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory
conversationId={conversationId}
setTitle={props.setTitle}
setAgent={setAgentMetadata}
pendingMessage={processingMessage ? message : ""}
incomingMessages={props.streamedMessages}
/>
</div>
<div
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`}
>
<ChatInputArea
agentColor={agentMetadata?.color}
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={conversationId}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles}
/>
</div>
</>
);
}
export default function Chat() {
const defaultTitle = "Khoj AI - Chat";
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true)
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState(defaultTitle);
const [conversationId, setConversationID] = useState<string | null>(null);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>("");
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const locationData = useIPLocationData();
const authenticatedData = useAuthenticatedData();
useEffect(() => {
fetch('/api/chat/options')
.then(response => response.json())
useEffect(() => {
fetch("/api/chat/options")
.then((response) => response.json())
.then((data: ChatOptions) => {
setLoading(false);
// Render chat options, if any
if (data) {
console.log(data);
setChatOptionsData(data);
}
})
.catch(err => {
.catch((err) => {
console.error(err);
return;
});
}, []);
welcomeConsole();
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 786);
});
}, []);
useEffect(() => {
if (queryToProcess) {
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: new Date().toISOString(),
rawQuery: queryToProcess || "",
};
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
}
}, [queryToProcess]);
useEffect(() => {
if (processQuerySignal) {
chat();
}
}, [processQuerySignal]);
async function readChatStream(response: Response) {
if (!response.ok) throw new Error(response.statusText);
if (!response.body) throw new Error("Response body is null");
const reader = response.body.getReader();
const decoder = new TextDecoder();
const eventDelimiter = "␃🔚␗";
let buffer = "";
// Track context used for chat response
let context: Context[] = [];
let onlineContext: OnlineContext = {};
while (true) {
const { done, value } = await reader.read();
if (done) {
setQueryToProcess("");
setProcessQuerySignal(false);
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
let newEventIndex;
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
const event = buffer.slice(0, newEventIndex);
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
if (event) {
const currentMessage = messages.find((message) => !message.completed);
if (!currentMessage) {
console.error("No current message found");
return;
}
// Track context used for chat response. References are rendered at the end of the chat
({ context, onlineContext } = processMessageChunk(
event,
currentMessage,
context,
onlineContext,
));
setMessages([...messages]);
}
}
}
}
async function chat() {
localStorage.removeItem("message");
if (!queryToProcess || !conversationId) return;
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`;
if (locationData) {
chatAPI += `&region=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`;
}
const response = await fetch(chatAPI);
try {
await readChatStream(response);
} catch (err) {
console.log(err);
}
}
const handleConversationIdChange = (newConversationId: string) => {
setConversationID(newConversationId);
};
if (isLoading) return <Loading />;
return (
<div className={styles.main + " " + styles.chatLayout}>
<div className={styles.sidePanel}>
<SidePanel />
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>
{`${defaultTitle}${!!title && title !== defaultTitle ? `: ${title}` : ""}`}
</title>
<div>
<SidePanel
conversationId={conversationId}
uploadedFiles={uploadedFiles}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={styles.chatBox}>
<title>
Khoj AI - Chat
</title>
<NavMenu selected="Chat" />
<div>
<div className={styles.chatBoxBody}>
{!isMobileWidth && (
<div
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}
>
{title && (
<h2
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}
>
{title}
</h2>
)}
</div>
)}
<Suspense fallback={<Loading />}>
<ChatBodyData chatOptionsData={chatOptionsData} />
<ChatBodyData
isLoggedIn={authenticatedData !== null}
streamedMessages={messages}
chatOptionsData={chatOptionsData}
setTitle={setTitle}
setQueryToProcess={setQueryToProcess}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth}
onConversationIdChange={handleConversationIdChange}
/>
</Suspense>
</div>
<div className={styles.inputBox}>
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
<button className={styles.inputBox}>Send</button>
</div>
</div>
</div>
)
);
}

View file

@ -1,6 +1,6 @@
'use client'
"use client";
import useSWR from 'swr'
import useSWR from "swr";
export interface UserProfile {
email: string;
@ -11,15 +11,76 @@ export interface UserProfile {
detail: string;
}
const userFetcher = () => window.fetch('/api/v1/user').then(res => res.json()).catch(err => console.log(err));
const fetcher = (url: string) =>
window
.fetch(url)
.then((res) => res.json())
.catch((err) => console.warn(err));
export function useAuthenticatedData() {
const { data, error } = useSWR<UserProfile>("/api/v1/user", fetcher, {
revalidateOnFocus: false,
});
const { data, error } = useSWR<UserProfile>('/api/v1/user', userFetcher, { revalidateOnFocus: false });
if (error) return null;
if (!data) return null;
if (data.detail === 'Forbidden') return null;
if (error || !data || data.detail === "Forbidden") return null;
return data;
}
export interface ModelOptions {
id: number;
name: string;
}
export interface SyncedContent {
computer: boolean;
github: boolean;
notion: boolean;
}
export interface UserConfig {
// user info
username: string;
user_photo: string | null;
is_active: boolean;
given_name: string;
phone_number: string;
is_phone_number_verified: boolean;
// user content settings
enabled_content_source: SyncedContent;
has_documents: boolean;
notion_token: string | null;
// user model settings
search_model_options: ModelOptions[];
selected_search_model_config: number;
chat_model_options: ModelOptions[];
selected_chat_model_config: number;
paint_model_options: ModelOptions[];
selected_paint_model_config: number;
voice_model_options: ModelOptions[];
selected_voice_model_config: number;
// user billing info
subscription_state: string;
subscription_renewal_date: string;
// server settings
khoj_cloud_subscription_url: string | undefined;
billing_enabled: boolean;
is_eleven_labs_enabled: boolean;
is_twilio_enabled: boolean;
khoj_version: string;
anonymous_mode: boolean;
notion_oauth_url: string;
detail: string;
}
export function useUserConfig(detailed: boolean = false) {
const url = `/api/settings?detailed=${detailed}`;
const {
data: userConfig,
error,
isLoading: isLoadingUserConfig,
} = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
if (error || !userConfig || userConfig?.detail === "Forbidden")
return { userConfig: null, isLoadingUserConfig };
return { userConfig, isLoadingUserConfig };
}

View file

@ -0,0 +1,332 @@
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
export interface RawReferenceData {
context?: Context[];
onlineContext?: OnlineContext;
}
export interface ResponseWithReferences {
context?: Context[];
online?: OnlineContext;
response?: string;
}
interface MessageChunk {
type: string;
data: string | object;
}
export function convertMessageChunkToJson(chunk: string): MessageChunk {
if (chunk.startsWith("{") && chunk.endsWith("}")) {
try {
const jsonChunk = JSON.parse(chunk);
if (!jsonChunk.type) {
return {
type: "message",
data: jsonChunk,
};
}
return jsonChunk;
} catch (error) {
return {
type: "message",
data: chunk,
};
}
} else if (chunk.length > 0) {
return {
type: "message",
data: chunk,
};
} else {
return {
type: "message",
data: "",
};
}
}
function handleJsonResponse(chunkData: any) {
const jsonData = chunkData as any;
if (jsonData.image || jsonData.detail) {
let responseWithReference = handleImageResponse(chunkData, true);
if (responseWithReference.response) return responseWithReference.response;
} else if (jsonData.response) {
return jsonData.response;
} else {
throw new Error("Invalid JSON response");
}
}
export function processMessageChunk(
rawChunk: string,
currentMessage: StreamMessage,
context: Context[] = [],
onlineContext: OnlineContext = {},
): { context: Context[]; onlineContext: OnlineContext } {
const chunk = convertMessageChunkToJson(rawChunk);
if (!currentMessage || !chunk || !chunk.type) return { context, onlineContext };
if (chunk.type === "status") {
console.log(`status: ${chunk.data}`);
const statusMessage = chunk.data as string;
currentMessage.trainOfThought.push(statusMessage);
} else if (chunk.type === "references") {
const references = chunk.data as RawReferenceData;
if (references.context) context = references.context;
if (references.onlineContext) onlineContext = references.onlineContext;
return { context, onlineContext };
} else if (chunk.type === "message") {
const chunkData = chunk.data;
if (chunkData !== null && typeof chunkData === "object") {
currentMessage.rawResponse += handleJsonResponse(chunkData);
} else if (
typeof chunkData === "string" &&
chunkData.trim()?.startsWith("{") &&
chunkData.trim()?.endsWith("}")
) {
try {
const jsonData = JSON.parse(chunkData.trim());
currentMessage.rawResponse += handleJsonResponse(jsonData);
} catch (e) {
currentMessage.rawResponse += JSON.stringify(chunkData);
}
} else {
currentMessage.rawResponse += chunkData;
}
} else if (chunk.type === "start_llm_response") {
console.log(`Started streaming: ${new Date()}`);
} else if (chunk.type === "end_llm_response") {
console.log(`Completed streaming: ${new Date()}`);
// Append any references after all the data has been streamed
if (onlineContext) currentMessage.onlineContext = onlineContext;
if (context) currentMessage.context = context;
// Mark current message streaming as completed
currentMessage.completed = true;
}
return { context, onlineContext };
}
export function handleImageResponse(imageJson: any, liveStream: boolean): ResponseWithReferences {
let rawResponse = "";
if (imageJson.image) {
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
// If response has image field, response is a generated image.
if (imageJson.intentType === "text-to-image") {
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
}
if (inferredQuery && !liveStream) {
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
}
}
let reference: ResponseWithReferences = {};
if (imageJson.context && imageJson.context.length > 0) {
const rawReferenceAsJson = imageJson.context;
if (rawReferenceAsJson instanceof Array) {
reference.context = rawReferenceAsJson;
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
reference.online = rawReferenceAsJson;
}
}
if (imageJson.detail) {
// The detail field contains the improved image prompt
rawResponse += imageJson.detail;
}
reference.response = rawResponse;
return reference;
}
export function modifyFileFilterForConversation(
conversationId: string | null,
filenames: string[],
setAddedFiles: (files: string[]) => void,
mode: "add" | "remove",
) {
if (!conversationId) {
console.error("No conversation ID provided");
return;
}
const method = mode === "add" ? "POST" : "DELETE";
const body = {
conversation_id: conversationId,
filenames: filenames,
};
const addUrl = `/api/chat/conversation/file-filters/bulk`;
fetch(addUrl, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => response.json())
.then((data) => {
console.log("ADDEDFILES DATA: ", data);
setAddedFiles(data);
})
.catch((err) => {
console.error(err);
return;
});
}
export async function createNewConversation(slug: string) {
try {
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, {
method: "POST",
});
if (!response.ok)
throw new Error(`Failed to fetch chat sessions with status: ${response.status}`);
const data = await response.json();
const conversationID = data.conversation_id;
if (!conversationID) throw new Error("Conversation ID not found in response");
return conversationID;
} catch (error) {
console.error("Error creating new conversation:", error);
throw error;
}
}
export function uploadDataForIndexing(
files: FileList,
setWarning: (warning: string) => void,
setUploading: (uploading: boolean) => void,
setError: (error: string) => void,
setUploadedFiles?: (files: string[]) => void,
conversationId?: string | null,
) {
const allowedExtensions = [
"text/org",
"text/markdown",
"text/plain",
"text/html",
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
];
const allowedFileEndings = ["org", "md", "txt", "html", "pdf", "docx"];
const badFiles: string[] = [];
const goodFiles: File[] = [];
const uploadedFiles: string[] = [];
for (let file of files) {
const fileEnding = file.name.split(".").pop();
if (!file || !file.name || !fileEnding) {
if (file) {
badFiles.push(file.name);
}
} else if (
!allowedExtensions.includes(file.type) &&
!allowedFileEndings.includes(fileEnding.toLowerCase())
) {
badFiles.push(file.name);
} else {
goodFiles.push(file);
}
}
if (goodFiles.length === 0) {
setWarning("No supported files found");
return;
}
if (badFiles.length > 0) {
setWarning("The following files are not supported yet:\n" + badFiles.join("\n"));
}
const formData = new FormData();
// Create an array of Promises for file reading
const fileReadPromises = Array.from(goodFiles).map((file) => {
return new Promise<void>((resolve, reject) => {
let reader = new FileReader();
reader.onload = function (event) {
if (event.target === null) {
reject();
return;
}
let fileContents = event.target.result;
let fileType = file.type;
let fileName = file.name;
if (fileType === "") {
let fileExtension = fileName.split(".").pop();
if (fileExtension === "org") {
fileType = "text/org";
} else if (fileExtension === "md") {
fileType = "text/markdown";
} else if (fileExtension === "txt") {
fileType = "text/plain";
} else if (fileExtension === "html") {
fileType = "text/html";
} else if (fileExtension === "pdf") {
fileType = "application/pdf";
} else {
// Skip this file if its type is not supported
resolve();
return;
}
}
if (fileContents === null) {
reject();
return;
}
let fileObj = new Blob([fileContents], { type: fileType });
formData.append("files", fileObj, file.name);
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
});
setUploading(true);
// Wait for all files to be read before making the fetch request
Promise.all(fileReadPromises)
.then(() => {
return fetch("/api/content?client=web", {
method: "PATCH",
body: formData,
});
})
.then((data) => {
for (let file of goodFiles) {
uploadedFiles.push(file.name);
if (conversationId && setUploadedFiles) {
modifyFileFilterForConversation(
conversationId,
[file.name],
setUploadedFiles,
"add",
);
}
}
if (setUploadedFiles) setUploadedFiles(uploadedFiles);
})
.catch((error) => {
console.log(error);
setError(`Error uploading file: ${error}`);
})
.finally(() => {
setUploading(false);
});
}

View file

@ -0,0 +1,55 @@
const tailwindColors = [
"red",
"yellow",
"green",
"blue",
"orange",
"purple",
"pink",
"teal",
"cyan",
"lime",
"indigo",
"fuschia",
"rose",
"sky",
"amber",
"emerald",
];
export function convertColorToTextClass(color: string) {
if (tailwindColors.includes(color)) {
return `text-${color}-500`;
}
return `text-gray-500`;
}
export function convertToBGGradientClass(color: string) {
if (tailwindColors.includes(color)) {
return `bg-gradient-to-b from-[hsl(var(--background))] to-${color}-100/70 dark:from-[hsl(var(--background))] dark:to-${color}-950/30 `;
}
return `bg-gradient-to-b from-white to-orange-50`;
}
export function convertToBGClass(color: string) {
if (tailwindColors.includes(color)) {
return `bg-${color}-500 dark:bg-${color}-900 hover:bg-${color}-400 dark:hover:bg-${color}-800`;
}
return `bg-background`;
}
export function converColorToBgGradient(color: string) {
return `${convertToBGGradientClass(color)} dark:border dark:border-neutral-700`;
}
export function convertColorToBorderClass(color: string) {
if (tailwindColors.includes(color)) {
return `border-${color}-500`;
}
return `border-gray-500`;
}
//rewrite colorMap using the convertColorToBorderClass function and iteration through the tailwindColors array
export const colorMap: Record<string, string> = {};
for (const color of tailwindColors) {
colorMap[color] = convertColorToBorderClass(color);
}

View file

@ -0,0 +1,124 @@
import React from "react";
import { convertColorToTextClass } from "./colorUtils";
import {
Lightbulb,
Robot,
Aperture,
GraduationCap,
Jeep,
Island,
MathOperations,
Asclepius,
Couch,
Code,
Atom,
ClockCounterClockwise,
File,
Globe,
Palette,
Book,
Confetti,
House,
Translate,
Image,
} from "@phosphor-icons/react";
import { Markdown, OrgMode, Pdf, Word } from "@/app/components/logo/fileLogo";
interface IconMap {
[key: string]: (color: string, width: string, height: string) => JSX.Element | null;
}
const iconMap: IconMap = {
Lightbulb: (color: string, width: string, height: string) => (
<Lightbulb className={`${width} ${height} ${color} mr-2`} />
),
Robot: (color: string, width: string, height: string) => (
<Robot className={`${width} ${height} ${color} mr-2`} />
),
Aperture: (color: string, width: string, height: string) => (
<Aperture className={`${width} ${height} ${color} mr-2`} />
),
GraduationCap: (color: string, width: string, height: string) => (
<GraduationCap className={`${width} ${height} ${color} mr-2`} />
),
Jeep: (color: string, width: string, height: string) => (
<Jeep className={`${width} ${height} ${color} mr-2`} />
),
Island: (color: string, width: string, height: string) => (
<Island className={`${width} ${height} ${color} mr-2`} />
),
MathOperations: (color: string, width: string, height: string) => (
<MathOperations className={`${width} ${height} ${color} mr-2`} />
),
Asclepius: (color: string, width: string, height: string) => (
<Asclepius className={`${width} ${height} ${color} mr-2`} />
),
Couch: (color: string, width: string, height: string) => (
<Couch className={`${width} ${height} ${color} mr-2`} />
),
Code: (color: string, width: string, height: string) => (
<Code className={`${width} ${height} ${color} mr-2`} />
),
Atom: (color: string, width: string, height: string) => (
<Atom className={`${width} ${height} ${color} mr-2`} />
),
ClockCounterClockwise: (color: string, width: string, height: string) => (
<ClockCounterClockwise className={`${width} ${height} ${color} mr-2`} />
),
Globe: (color: string, width: string, height: string) => (
<Globe className={`${width} ${height} ${color} mr-2`} />
),
Palette: (color: string, width: string, height: string) => (
<Palette className={`${width} ${height} ${color} mr-2`} />
),
Book: (color: string, width: string, height: string) => (
<Book className={`${width} ${height} ${color} mr-2`} />
),
Confetti: (color: string, width: string, height: string) => (
<Confetti className={`${width} ${height} ${color} mr-2`} />
),
House: (color: string, width: string, height: string) => (
<House className={`${width} ${height} ${color} mr-2`} />
),
Translate: (color: string, width: string, height: string) => (
<Translate className={`${width} ${height} ${color} mr-2`} />
),
};
function getIconFromIconName(
iconName: string,
color: string = "gray",
width: string = "w-6",
height: string = "h-6",
) {
const icon = iconMap[iconName];
const colorName = color.toLowerCase();
const colorClass = convertColorToTextClass(colorName);
return icon ? icon(colorClass, width, height) : null;
}
function getIconFromFilename(
filename: string,
className: string = "w-6 h-6 text-muted-foreground inline-flex mr-1",
) {
const extension = filename.split(".").pop();
switch (extension) {
case "org":
return <OrgMode className={className} />;
case "markdown":
case "md":
return <Markdown className={className} />;
case "pdf":
return <Pdf className={className} />;
case "doc":
return <Word className={className} />;
case "jpg":
case "jpeg":
case "png":
return <Image className={className} weight="fill" />;
default:
return <File className={className} weight="fill" />;
}
}
export { getIconFromIconName, getIconFromFilename };

View file

@ -0,0 +1,56 @@
import useSWR from "swr";
export interface LocationData {
ip: string;
city: string;
region: string;
country: string;
postal: string;
latitude: number;
longitude: number;
timezone: string;
}
const locationFetcher = () =>
window
.fetch("https://ipapi.co/json")
.then((res) => res.json())
.catch((err) => console.log(err));
export const toTitleCase = (str: string) =>
str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
export function welcomeConsole() {
console.log(
`%c %s`,
"font-family:monospace",
`
__ __ __ __ ______ __ _____ __
/\\ \\/ / /\\ \\_\\ \\ /\\ __ \\ /\\ \\ /\\ __ \\ /\\ \\
\\ \\ _"-. \\ \\ __ \\ \\ \\ \\/\\ \\ _\\_\\ \\ \\ \\ __ \\ \\ \\ \\
\\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_____\\ /\\_____\\ \\ \\_\\ \\_\\ \\ \\_\\
\\/_/\\/_/ \\/_/\\/_/ \\/_____/ \\/_____/ \\/_/\\/_/ \\/_/
Greetings traveller,
I am Khoj, your open-source, personal AI copilot.
See my source code at https://github.com/khoj-ai/khoj
Read my operating manual at https://docs.khoj.dev
`,
);
}
export function useIPLocationData() {
const { data: locationData, error: locationDataError } = useSWR<LocationData>(
"/api/ip",
locationFetcher,
{ revalidateOnFocus: false },
);
if (locationDataError) return null;
if (!locationData) return null;
return locationData;
}

View file

@ -7,6 +7,19 @@ div.chatHistory {
div.chatLayout {
height: 80vh;
overflow-y: auto;
/* width: 80%; */
margin: 0 auto;
}
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.trainOfThought {
border: 1px var(--border-color) solid;
border-radius: 16px;
padding: 8px 16px;
margin: 12px;
}

View file

@ -1,13 +1,24 @@
'use client'
"use client";
import styles from './chatHistory.module.css';
import { useRef, useEffect, useState } from 'react';
import styles from "./chatHistory.module.css";
import { useRef, useEffect, useState } from "react";
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
import ChatMessage, {
ChatHistoryData,
StreamMessage,
TrainOfThought,
} from "../chatMessage/chatMessage";
import renderMathInElement from 'katex/contrib/auto-render';
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css'
import { ScrollArea } from "@/components/ui/scroll-area";
import { InlineLoading } from "../loading/loading";
import { Lightbulb } from "@phosphor-icons/react";
import ProfileCard from "../profileCard/profileCard";
import { getIconFromIconName } from "@/app/common/iconUtils";
import { AgentData } from "@/app/agents/page";
import React from "react";
interface ChatResponse {
status: string;
@ -15,86 +26,320 @@ interface ChatResponse {
}
interface ChatHistory {
[key: string]: string
[key: string]: string;
}
interface ChatHistoryProps {
conversationId: string;
setReferencePanelData: Function;
setShowReferencePanel: Function;
setTitle: (title: string) => void;
incomingMessages?: StreamMessage[];
pendingMessage?: string;
publicConversationSlug?: string;
setAgent: (agent: AgentData) => void;
}
function constructTrainOfThought(
trainOfThought: string[],
lastMessage: boolean,
agentColor: string,
key: string,
completed: boolean = false,
) {
const lastIndex = trainOfThought.length - 1;
return (
<div className={`${styles.trainOfThought} shadow-sm`} key={key}>
{!completed && <InlineLoading className="float-right" />}
{trainOfThought.map((train, index) => (
<TrainOfThought
key={`train-${index}`}
message={train}
primary={index === lastIndex && lastMessage && !completed}
agentColor={agentColor}
/>
))}
</div>
);
}
export default function ChatHistory(props: ChatHistoryProps) {
const [data, setData] = useState<ChatHistoryData | null>(null);
const [isLoading, setLoading] = useState(true)
const [currentPage, setCurrentPage] = useState(0);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
const ref = useRef<HTMLDivElement>(null);
const chatHistoryRef = useRef(null);
useEffect(() => {
fetch(`/api/chat/history?client=web&conversation_id=${props.conversationId}&n=10`)
.then(response => response.json())
.then((chatData: ChatResponse) => {
setLoading(false);
// Render chat options, if any
if (chatData) {
console.log(chatData);
setData(chatData.response);
}
})
.catch(err => {
console.error(err);
return;
});
}, [props.conversationId]);
const chatHistoryRef = useRef<HTMLDivElement | null>(null);
const sentinelRef = useRef<HTMLDivElement | null>(null);
const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState<
number | null
>(null);
const [fetchingData, setFetchingData] = useState(false);
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => {
const observer = new MutationObserver((mutationsList, observer) => {
// If the addedNodes property has one or more nodes
for(let mutation of mutationsList) {
if(mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Call your function here
renderMathInElement(document.body, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '\\[', right: '\\]', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
],
});
}
}
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 768);
});
if (chatHistoryRef.current) {
observer.observe(chatHistoryRef.current, { childList: true });
}
// Clean up the observer on component unmount
return () => observer.disconnect();
setIsMobileWidth(window.innerWidth < 768);
}, []);
if (isLoading) {
return <h2>🌀 Loading...</h2>;
useEffect(() => {
// This function ensures that scrolling to bottom happens after the data (chat messages) has been updated and rendered the first time.
const scrollToBottomAfterDataLoad = () => {
// Assume the data is loading in this scenario.
if (!data?.chat.length) {
setTimeout(() => {
scrollToBottom();
}, 500);
}
};
if (currentPage < 2) {
// Call the function defined above.
scrollToBottomAfterDataLoad();
}
}, [data, currentPage]);
useEffect(() => {
if (!hasMoreMessages || fetchingData) return;
// TODO: A future optimization would be to add a time to delay to re-enabling the intersection observer.
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMoreMessages) {
setFetchingData(true);
fetchMoreMessages(currentPage);
setCurrentPage((prev) => prev + 1);
}
},
{ threshold: 1.0 },
);
if (sentinelRef.current) {
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [hasMoreMessages, currentPage, fetchingData]);
useEffect(() => {
setHasMoreMessages(true);
setFetchingData(false);
setCurrentPage(0);
setData(null);
}, [props.conversationId]);
useEffect(() => {
if (props.incomingMessages) {
const lastMessage = props.incomingMessages[props.incomingMessages.length - 1];
if (lastMessage && !lastMessage.completed) {
setIncompleteIncomingMessageIndex(props.incomingMessages.length - 1);
}
}
if (isUserAtBottom()) {
scrollToBottom();
}
}, [props.incomingMessages]);
function fetchMoreMessages(currentPage: number) {
if (!hasMoreMessages || fetchingData) return;
const nextPage = currentPage + 1;
let conversationFetchURL = "";
if (props.conversationId) {
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`;
} else if (props.publicConversationSlug) {
conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`;
} else {
return;
}
fetch(conversationFetchURL)
.then((response) => response.json())
.then((chatData: ChatResponse) => {
props.setTitle(chatData.response.slug);
if (
chatData &&
chatData.response &&
chatData.response.chat &&
chatData.response.chat.length > 0
) {
if (chatData.response.chat.length === data?.chat.length) {
setHasMoreMessages(false);
setFetchingData(false);
return;
}
props.setAgent(chatData.response.agent);
setData(chatData.response);
if (currentPage < 2) {
scrollToBottom();
}
setFetchingData(false);
} else {
if (chatData.response.agent && chatData.response.conversation_id) {
const chatMetadata = {
chat: [],
agent: chatData.response.agent,
conversation_id: chatData.response.conversation_id,
slug: chatData.response.slug,
};
props.setAgent(chatData.response.agent);
setData(chatMetadata);
}
setHasMoreMessages(false);
setFetchingData(false);
}
})
.catch((err) => {
console.error(err);
window.location.href = "/";
});
}
const scrollToBottom = () => {
if (chatHistoryRef.current) {
chatHistoryRef.current.scrollIntoView(false);
}
};
const isUserAtBottom = () => {
if (!chatHistoryRef.current) return false;
// NOTE: This isn't working. It always seems to return true. This is because
const { scrollTop, scrollHeight, clientHeight } = chatHistoryRef.current as HTMLDivElement;
const threshold = 25; // pixels from the bottom
// Considered at the bottom if within threshold pixels from the bottom
return scrollTop + clientHeight >= scrollHeight - threshold;
};
function constructAgentLink() {
if (!data || !data.agent || !data.agent.slug) return `/agents`;
return `/agents?agent=${data.agent.slug}`;
}
function constructAgentName() {
if (!data || !data.agent || !data.agent.name) return `Agent`;
return data.agent.name;
}
function constructAgentPersona() {
if (!data || !data.agent || !data.agent.persona) return ``;
return data.agent.persona;
}
if (!props.conversationId && !props.publicConversationSlug) {
return null;
}
return (
<div className={styles.main + " " + styles.chatLayout}>
<ScrollArea className={`h-[80vh]`}>
<div ref={ref}>
<div className={styles.chatHistory} ref={chatHistoryRef}>
{(data && data.chat) && data.chat.map((chatMessage, index) => (
<div ref={sentinelRef} style={{ height: "1px" }}>
{fetchingData && (
<InlineLoading message="Loading Conversation" className="opacity-50" />
)}
</div>
{data &&
data.chat &&
data.chat.map((chatMessage, index) => (
<ChatMessage
key={`${index}fullHistory`}
isMobileWidth={isMobileWidth}
chatMessage={chatMessage}
customClassName="fullHistory"
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={index === data.chat.length - 1}
/>
))}
{props.incomingMessages &&
props.incomingMessages.map((message, index) => {
return (
<React.Fragment key={`incomingMessage${index}`}>
<ChatMessage
key={`${index}outgoing`}
isMobileWidth={isMobileWidth}
chatMessage={{
message: message.rawQuery,
context: [],
onlineContext: {},
created: message.timestamp,
by: "you",
automationId: "",
}}
customClassName="fullHistory"
borderLeftColor={`${data?.agent.color}-500`}
/>
{message.trainOfThought &&
constructTrainOfThought(
message.trainOfThought,
index === incompleteIncomingMessageIndex,
data?.agent.color || "orange",
`${index}trainOfThought`,
message.completed,
)}
<ChatMessage
key={`${index}incoming`}
isMobileWidth={isMobileWidth}
chatMessage={{
message: message.rawResponse,
context: message.context,
onlineContext: message.onlineContext,
created: message.timestamp,
by: "khoj",
automationId: "",
rawQuery: message.rawQuery,
}}
customClassName="fullHistory"
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={true}
/>
</React.Fragment>
);
})}
{props.pendingMessage && (
<ChatMessage
key={index}
chatMessage={chatMessage}
setReferencePanelData={props.setReferencePanelData}
setShowReferencePanel={props.setShowReferencePanel}
key={`pendingMessage-${props.pendingMessage.length}`}
isMobileWidth={isMobileWidth}
chatMessage={{
message: props.pendingMessage,
context: [],
onlineContext: {},
created: new Date().getTime().toString(),
by: "you",
automationId: "",
}}
customClassName="fullHistory"
borderLeftColor={`${data?.agent.color}-500`}
isLastMessage={true}
/>
))}
)}
{data && (
<div className={`${styles.agentIndicator} pb-4`}>
<div className="relative group mx-2 cursor-pointer">
<ProfileCard
name={constructAgentName()}
link={constructAgentLink()}
avatar={
getIconFromIconName(data.agent.icon, data.agent.color) || (
<Lightbulb />
)
}
description={constructAgentPersona()}
/>
</div>
</div>
)}
</div>
</div>
</div>
)
</ScrollArea>
);
}

View file

@ -0,0 +1,4 @@
div.actualInputArea {
display: grid;
grid-template-columns: auto 1fr auto auto;
}

View file

@ -0,0 +1,465 @@
import styles from "./chatInputArea.module.css";
import React, { useEffect, useRef, useState } from "react";
import { uploadDataForIndexing } from "../../common/chatFunctions";
import { Progress } from "@/components/ui/progress";
import "katex/dist/katex.min.css";
import {
ArrowRight,
ArrowUp,
Browser,
ChatsTeardrop,
GlobeSimple,
Gps,
Image,
Microphone,
Notebook,
Paperclip,
Question,
Robot,
Shapes,
Stop,
} from "@phosphor-icons/react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Popover, PopoverContent } from "@/components/ui/popover";
import { PopoverTrigger } from "@radix-ui/react-popover";
import LoginPrompt from "../loginPrompt/loginPrompt";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { InlineLoading } from "../loading/loading";
import { convertToBGClass } from "@/app/common/colorUtils";
export interface ChatOptions {
[key: string]: string;
}
interface ChatInputProps {
sendMessage: (message: string) => void;
sendDisabled: boolean;
setUploadedFiles?: (files: string[]) => void;
conversationId?: string | null;
chatOptionsData?: ChatOptions | null;
isMobileWidth?: boolean;
isLoggedIn: boolean;
agentColor?: string;
}
export default function ChatInputArea(props: ChatInputProps) {
const [message, setMessage] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [loginRedirectMessage, setLoginRedirectMessage] = useState<string | null>(null);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [recording, setRecording] = useState(false);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
const [progressValue, setProgressValue] = useState(0);
useEffect(() => {
if (!uploading) {
setProgressValue(0);
}
if (uploading) {
const interval = setInterval(() => {
setProgressValue((prev) => {
const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5
const nextValue = prev + increment;
return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100
});
}, 800);
return () => clearInterval(interval);
}
}, [uploading]);
function onSendMessage() {
if (!message.trim()) return;
if (!props.isLoggedIn) {
setLoginRedirectMessage(
"Hey there, you need to be signed in to send messages to Khoj AI",
);
setShowLoginPrompt(true);
return;
}
props.sendMessage(message.trim());
setMessage("");
}
function handleSlashCommandClick(command: string) {
setMessage(`/${command} `);
}
function handleFileButtonClick() {
if (!fileInputRef.current) return;
fileInputRef.current.click();
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
if (!event.target.files) return;
if (!props.isLoggedIn) {
setLoginRedirectMessage("Whoa! You need to login to upload files");
setShowLoginPrompt(true);
return;
}
uploadDataForIndexing(
event.target.files,
setWarning,
setUploading,
setError,
props.setUploadedFiles,
props.conversationId,
);
}
function getIconForSlashCommand(command: string) {
const className = "h-4 w-4 mr-2";
if (command.includes("summarize")) {
return <Gps className={className} />;
}
if (command.includes("help")) {
return <Question className={className} />;
}
if (command.includes("automation")) {
return <Robot className={className} />;
}
if (command.includes("webpage")) {
return <Browser className={className} />;
}
if (command.includes("notes")) {
return <Notebook className={className} />;
}
if (command.includes("image")) {
return <Image className={className} />;
}
if (command.includes("default")) {
return <Shapes className={className} />;
}
if (command.includes("general")) {
return <ChatsTeardrop className={className} />;
}
if (command.includes("online")) {
return <GlobeSimple className={className} />;
}
return <ArrowRight className={className} />;
}
// Assuming this function is added within the same context as the provided excerpt
async function startRecordingAndTranscribe() {
try {
const microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(microphone, { mimeType: "audio/webm" });
const audioChunks: Blob[] = [];
mediaRecorder.ondataavailable = async (event) => {
audioChunks.push(event.data);
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
const formData = new FormData();
formData.append("file", audioBlob);
// Send the incremental audio blob to the server
try {
const response = await fetch("/api/transcribe", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const transcription = await response.json();
setMessage(transcription.text.trim());
} catch (error) {
console.error("Error sending audio to server:", error);
}
};
// Send an audio blob every 1.5 seconds
mediaRecorder.start(1500);
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
const formData = new FormData();
formData.append("file", audioBlob);
// Send the audio blob to the server
try {
const response = await fetch("/api/transcribe", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const transcription = await response.json();
mediaRecorder.stream.getTracks().forEach((track) => track.stop());
setMediaRecorder(null);
setMessage(transcription.text.trim());
} catch (error) {
console.error("Error sending audio to server:", error);
}
};
setMediaRecorder(mediaRecorder);
} catch (error) {
console.error("Error getting microphone", error);
}
}
useEffect(() => {
if (!recording && mediaRecorder) {
mediaRecorder.stop();
}
if (recording && !mediaRecorder) {
startRecordingAndTranscribe();
}
}, [recording, mediaRecorder]);
const chatInputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (!chatInputRef.current) return;
chatInputRef.current.style.height = "auto";
chatInputRef.current.style.height =
Math.max(chatInputRef.current.scrollHeight - 24, 64) + "px";
}, [message]);
return (
<>
{showLoginPrompt && loginRedirectMessage && (
<LoginPrompt
onOpenChange={setShowLoginPrompt}
loginRedirectMessage={loginRedirectMessage}
/>
)}
{uploading && (
<AlertDialog open={uploading}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Progress
indicatorColor="bg-slate-500"
className="w-full h-2 rounded-full"
value={progressValue}
/>
</AlertDialogDescription>
<AlertDialogAction
className="bg-slate-400 hover:bg-slate-500"
onClick={() => setUploading(false)}
>
Dismiss
</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)}
{warning && (
<AlertDialog open={warning !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>{warning}</AlertDialogDescription>
<AlertDialogAction
className="bg-slate-400 hover:bg-slate-500"
onClick={() => setWarning(null)}
>
Close
</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)}
{error && (
<AlertDialog open={error !== null}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Oh no!</AlertDialogTitle>
<AlertDialogDescription>
Something went wrong while uploading your data
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogDescription>{error}</AlertDialogDescription>
<AlertDialogAction
className="bg-slate-400 hover:bg-slate-500"
onClick={() => setError(null)}
>
Close
</AlertDialogAction>
</AlertDialogContent>
</AlertDialog>
)}
{message.startsWith("/") && message.split(" ").length === 1 && (
<div className="flex justify-center text-center">
<Popover open={message.startsWith("/")}>
<PopoverTrigger className="flex justify-center text-center"></PopoverTrigger>
<PopoverContent
onOpenAutoFocus={(e) => e.preventDefault()}
className={`${props.isMobileWidth ? "w-[100vw]" : "w-full"} rounded-md`}
>
<Command className="max-w-full">
<CommandInput
placeholder="Type a command or search..."
value={message}
className="hidden"
/>
<CommandList>
<CommandEmpty>No matching commands.</CommandEmpty>
<CommandGroup heading="Agent Tools">
{props.chatOptionsData &&
Object.entries(props.chatOptionsData).map(
([key, value]) => (
<CommandItem
key={key}
className={`text-md`}
onSelect={() =>
handleSlashCommandClick(key)
}
>
<div className="grid grid-cols-1 gap-1">
<div className="font-bold flex items-center">
{getIconForSlashCommand(key)}/{key}
</div>
<div>{value}</div>
</div>
</CommandItem>
),
)}
</CommandGroup>
<CommandSeparator />
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}
<div
className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700`}
>
<input
type="file"
multiple={true}
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: "none" }}
/>
<Button
variant={"ghost"}
className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled}
onClick={handleFileButtonClick}
>
<Paperclip className="w-8 h-8" />
</Button>
<div className="grid w-full gap-1.5 relative">
<Textarea
ref={chatInputRef}
className={`border-none w-full h-16 min-h-16 max-h-[128px] md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? "text-md" : "text-lg"}`}
placeholder="Type / to see a list of commands"
id="message"
autoFocus={true}
value={message}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onSendMessage();
}
}}
onChange={(e) => setMessage(e.target.value)}
disabled={props.sendDisabled || recording}
/>
</div>
{recording ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
className={`${!recording && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={() => {
setRecording(!recording);
}}
disabled={props.sendDisabled}
>
<Stop weight="fill" className="w-6 h-6" />
</Button>
</TooltipTrigger>
<TooltipContent>
Click to stop recording and transcribe your voice.
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : mediaRecorder ? (
<InlineLoading />
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
className={`${!message || recording || "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={() => {
setMessage("Listening...");
setRecording(!recording);
}}
disabled={props.sendDisabled}
>
<Microphone weight="fill" className="w-6 h-6" />
</Button>
</TooltipTrigger>
<TooltipContent>
Click to transcribe your message with voice.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Button
className={`${(!message || recording) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={onSendMessage}
disabled={props.sendDisabled}
>
<ArrowUp className="w-6 h-6" weight="bold" />
</Button>
</div>
</>
);
}

View file

@ -1,20 +1,50 @@
div.chatMessageContainer {
display: flex;
flex-direction: column;
margin: 12px;
border-radius: 16px;
padding: 8px 16px 0 16px;
}
div.chatMessageWrapper {
padding-left: 1rem;
padding-bottom: 1rem;
max-width: 80vw;
}
div.chatMessageWrapper p:not(:last-child) {
margin-bottom: 16px;
}
div.khojfullHistory {
border-width: 1px;
padding-left: 4px;
}
div.youfullHistory {
max-width: 100%;
}
div.chatMessageContainer.youfullHistory {
padding-left: 0px;
}
div.you {
color: var(--frosted-background-color);
background-color: var(--intense-green);
background-color: hsla(var(--secondary));
align-self: flex-end;
border-radius: 16px;
}
div.khoj {
background-color: transparent;
color: #000000;
color: hsl(var(--accent-foreground));
align-self: flex-start;
}
div.khojChatMessage {
padding-top: 8px;
padding-left: 16px;
}
div.chatMessageContainer img {
width: 50%;
}
@ -23,8 +53,8 @@ div.chatMessageContainer h3 img {
width: 24px;
}
div.you .author {
color: var(--frosted-background-color);
div.you {
color: hsla(var(--secondary-foreground));
}
div.author {
@ -36,31 +66,32 @@ div.author {
div.chatFooter {
display: flex;
justify-content: space-between;
margin-top: 8px;
min-height: 28px;
}
div.chatButtons {
display: flex;
justify-content: flex-end;
width: min-content;
border: var(--border-color) 1px solid;
border-radius: 16px;
position: relative;
bottom: -12px;
background-color: hsla(var(--secondary));
}
div.chatFooter button {
cursor: pointer;
background-color: var(--calm-blue);
color: var(--main-text-color);
color: hsl(var(--muted-foreground));
border: none;
border-radius: 0.5rem;
padding: 0.25rem;
margin-left: 0.5rem;
border-radius: 16px;
padding: 4px;
margin-left: 4px;
margin-right: 4px;
}
div.chatFooter button:hover {
background-color: var(--frosted-background-color);
color: var(--intense-green);
}
div.chatTimestamp {
font-size: small;
background-color: hsla(var(--frosted-background-color));
}
button.codeCopyButton {
@ -70,11 +101,35 @@ button.codeCopyButton {
}
button.codeCopyButton:hover {
background-color: var(--intense-green);
color: var(--frosted-background-color);
color: hsla(var(--frosted-background-color));
}
div.feedbackButtons img,
button.codeCopyButton img,
button.copyButton img {
width: 24px;
}
div.trainOfThought strong {
font-weight: 500;
}
div.trainOfThought.primary strong {
font-weight: 500;
color: hsla(var(--secondary-foreground));
}
div.trainOfThought.primary p {
color: inherit;
}
div.trainOfThoughtElement {
display: grid;
grid-template-columns: auto 1fr;
}
@media screen and (max-width: 768px) {
div.youfullHistory {
max-width: 90%;
}
}

View file

@ -1,26 +1,47 @@
"use client"
"use client";
import styles from './chatMessage.module.css';
import styles from "./chatMessage.module.css";
import markdownIt from 'markdown-it';
import markdownIt from "markdown-it";
import mditHljs from "markdown-it-highlightjs";
import React, { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
import React, { useEffect, useRef, useState } from "react";
import 'katex/dist/katex.min.css';
import 'highlight.js/styles/github.css'
import "katex/dist/katex.min.css";
import { hasValidReferences } from '../referencePanel/referencePanel';
import { TeaserReferencesSection, constructAllReferences } from "../referencePanel/referencePanel";
import {
ThumbsUp,
ThumbsDown,
Copy,
Brain,
Cloud,
Folder,
Book,
Aperture,
SpeakerHigh,
MagnifyingGlass,
Pause,
Palette,
} from "@phosphor-icons/react";
import DOMPurify from "dompurify";
import { InlineLoading } from "../loading/loading";
import { convertColorToTextClass } from "@/app/common/colorUtils";
import { AgentData } from "@/app/agents/page";
import renderMathInElement from "katex/contrib/auto-render";
import "katex/dist/katex.min.css";
const md = new markdownIt({
html: true,
linkify: true,
typographer: true
typographer: true,
});
md.use(mditHljs, {
inline: true,
code: true
code: true,
});
export interface Context {
@ -28,6 +49,10 @@ export interface Context {
file: string;
}
export interface OnlineContext {
[key: string]: OnlineContextData;
}
export interface WebPage {
link: string;
query: string;
@ -53,45 +78,50 @@ export interface OnlineContextData {
answer: string;
source: string;
title: string;
}
};
knowledgeGraph: {
attributes: {
[key: string]: string;
}
};
description: string;
descriptionLink: string;
descriptionSource: string;
imageUrl: string;
title: string;
type: string;
}
};
organic: OrganicContext[];
peopleAlsoAsk: PeopleAlsoAsk[];
}
interface AgentData {
name: string;
avatar: string;
slug: string;
}
interface Intent {
type: string;
query: string;
"memory-type": string;
"inferred-queries": string[];
}
export interface SingleChatMessage {
automationId: string;
by: string;
intent: {
[key: string]: string
}
message: string;
context: Context[];
created: string;
onlineContext: {
[key: string]: OnlineContextData
}
context: Context[];
onlineContext: OnlineContext;
rawQuery?: string;
intent?: Intent;
agent?: AgentData;
}
export interface StreamMessage {
rawResponse: string;
trainOfThought: string[];
context: Context[];
onlineContext: OnlineContext;
completed: boolean;
rawQuery: string;
timestamp: string;
agent?: AgentData;
}
export interface ChatHistoryData {
@ -101,98 +131,193 @@ export interface ChatHistoryData {
slug: string;
}
function FeedbackButtons() {
return (
<div className={styles.feedbackButtons}>
<button className={styles.thumbsUpButton}>
<Image
src="/thumbs-up.svg"
alt="Thumbs Up"
width={24}
height={24}
priority
/>
</button>
<button className={styles.thumbsDownButton}>
<Image
src="/thumbs-down.svg"
alt="Thumbs Down"
width={24}
height={24}
priority
/>
</button>
</div>
)
function sendFeedback(uquery: string, kquery: string, sentiment: string) {
fetch("/api/chat/feedback", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ uquery: uquery, kquery: kquery, sentiment: sentiment }),
});
}
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
event.preventDefault();
setReferencePanelData(chatMessage);
setShowReferencePanel(true);
function FeedbackButtons({ uquery, kquery }: { uquery: string; kquery: string }) {
return (
<div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
<button
title="Like"
className={styles.thumbsUpButton}
onClick={() => sendFeedback(uquery, kquery, "positive")}
>
<ThumbsUp alt="Like Message" color="hsl(var(--muted-foreground))" />
</button>
<button
title="Dislike"
className={styles.thumbsDownButton}
onClick={() => sendFeedback(uquery, kquery, "negative")}
>
<ThumbsDown alt="Dislike Message" color="hsl(var(--muted-foreground))" />
</button>
</div>
);
}
interface ChatMessageProps {
chatMessage: SingleChatMessage;
setReferencePanelData: Function;
setShowReferencePanel: Function;
isMobileWidth: boolean;
customClassName?: string;
borderLeftColor?: string;
isLastMessage?: boolean;
agent?: AgentData;
}
interface TrainOfThoughtProps {
message: string;
primary: boolean;
agentColor: string;
}
function chooseIconFromHeader(header: string, iconColor: string) {
const compareHeader = header.toLowerCase();
const classNames = `inline mt-1 mr-2 ${iconColor} h-4 w-4`;
if (compareHeader.includes("understanding")) {
return <Brain className={`${classNames}`} />;
}
if (compareHeader.includes("generating")) {
return <Cloud className={`${classNames}`} />;
}
if (compareHeader.includes("data sources")) {
return <Folder className={`${classNames}`} />;
}
if (compareHeader.includes("notes")) {
return <Folder className={`${classNames}`} />;
}
if (compareHeader.includes("read")) {
return <Book className={`${classNames}`} />;
}
if (compareHeader.includes("search")) {
return <MagnifyingGlass className={`${classNames}`} />;
}
if (
compareHeader.includes("summary") ||
compareHeader.includes("summarize") ||
compareHeader.includes("enhanc")
) {
return <Aperture className={`${classNames}`} />;
}
if (compareHeader.includes("paint")) {
return <Palette className={`${classNames}`} />;
}
return <Brain className={`${classNames}`} />;
}
export function TrainOfThought(props: TrainOfThoughtProps) {
// The train of thought comes in as a markdown-formatted string. It starts with a heading delimited by two asterisks at the start and end and a colon, followed by the message. Example: **header**: status. This function will parse the message and render it as a div.
let extractedHeader = props.message.match(/\*\*(.*)\*\*/);
let header = extractedHeader ? extractedHeader[1] : "";
const iconColor = props.primary ? convertColorToTextClass(props.agentColor) : "text-gray-500";
const icon = chooseIconFromHeader(header, iconColor);
let markdownRendered = DOMPurify.sanitize(md.render(props.message));
return (
<div
className={`${styles.trainOfThoughtElement} items-center ${props.primary ? "text-gray-400" : "text-gray-300"} ${styles.trainOfThought} ${props.primary ? styles.primary : ""}`}
>
{icon}
<div dangerouslySetInnerHTML={{ __html: markdownRendered }} />
</div>
);
}
export default function ChatMessage(props: ChatMessageProps) {
const [copySuccess, setCopySuccess] = useState<boolean>(false);
const [isHovering, setIsHovering] = useState<boolean>(false);
const [markdownRendered, setMarkdownRendered] = useState<string>("");
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [interrupted, setInterrupted] = useState<boolean>(false);
let message = props.chatMessage.message;
// Replace LaTeX delimiters with placeholders
message = message.replace(/\\\(/g, 'LEFTPAREN').replace(/\\\)/g, 'RIGHTPAREN')
.replace(/\\\[/g, 'LEFTBRACKET').replace(/\\\]/g, 'RIGHTBRACKET');
if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image2") {
message = `![generated_image](${message})\n\n${props.chatMessage.intent["inferred-queries"][0]}`
}
let markdownRendered = md.render(message);
// Replace placeholders with LaTeX delimiters
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
const interruptedRef = useRef<boolean>(false);
const messageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (messageRef.current) {
const preElements = messageRef.current.querySelectorAll('pre > .hljs');
preElements.forEach((preElement) => {
const copyButton = document.createElement('button');
const copyImage = document.createElement('img');
copyImage.src = '/copy-button.svg';
copyImage.alt = 'Copy';
copyImage.width = 24;
copyImage.height = 24;
copyButton.appendChild(copyImage);
copyButton.className = `hljs ${styles.codeCopyButton}`
copyButton.addEventListener('click', () => {
let textContent = preElement.textContent || '';
// Strip any leading $ characters
textContent = textContent.replace(/^\$+/, '');
// Remove 'Copy' if it's at the start of the string
textContent = textContent.replace(/^Copy/, '');
textContent = textContent.trim();
navigator.clipboard.writeText(textContent);
});
preElement.prepend(copyButton);
});
}
}, [markdownRendered]);
interruptedRef.current = interrupted;
}, [interrupted]);
function renderTimeStamp(timestamp: string) {
var dateObject = new Date(timestamp);
var month = dateObject.getMonth() + 1;
var date = dateObject.getDate();
var year = dateObject.getFullYear();
const formattedDate = `${month}/${date}/${year}`;
return `${formattedDate} ${dateObject.toLocaleTimeString()}`;
}
useEffect(() => {
const observer = new MutationObserver((mutationsList, observer) => {
// If the addedNodes property has one or more nodes
if (messageRef.current) {
for (let mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
// Call your function here
renderMathInElement(messageRef.current, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "\\[", right: "\\]", display: true },
{ left: "$", right: "$", display: false },
{ left: "\\(", right: "\\)", display: false },
],
});
}
}
}
});
if (messageRef.current) {
observer.observe(messageRef.current, { childList: true });
}
// Clean up the observer on component unmount
return () => observer.disconnect();
}, [messageRef.current]);
useEffect(() => {
let message = props.chatMessage.message;
// Replace LaTeX delimiters with placeholders
message = message
.replace(/\\\(/g, "LEFTPAREN")
.replace(/\\\)/g, "RIGHTPAREN")
.replace(/\\\[/g, "LEFTBRACKET")
.replace(/\\\]/g, "RIGHTBRACKET");
if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image") {
message = `![generated image](data:image/png;base64,${message})`;
} else if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image2") {
message = `![generated image](${message})`;
} else if (
props.chatMessage.intent &&
props.chatMessage.intent.type == "text-to-image-v3"
) {
message = `![generated image](data:image/webp;base64,${message})`;
}
if (
props.chatMessage.intent &&
props.chatMessage.intent.type.includes("text-to-image") &&
props.chatMessage.intent["inferred-queries"]?.length > 0
) {
message += `\n\n**Inferred Query**\n\n${props.chatMessage.intent["inferred-queries"][0]}`;
}
let markdownRendered = md.render(message);
// Replace placeholders with LaTeX delimiters
markdownRendered = markdownRendered
.replace(/LEFTPAREN/g, "\\(")
.replace(/RIGHTPAREN/g, "\\)")
.replace(/LEFTBRACKET/g, "\\[")
.replace(/RIGHTBRACKET/g, "\\]");
// Sanitize and set the rendered markdown
setMarkdownRendered(DOMPurify.sanitize(markdownRendered));
}, [props.chatMessage.message, props.chatMessage.intent]);
useEffect(() => {
if (copySuccess) {
@ -202,57 +327,270 @@ export default function ChatMessage(props: ChatMessageProps) {
}
}, [copySuccess]);
let referencesValid = hasValidReferences(props.chatMessage);
useEffect(() => {
if (messageRef.current) {
const preElements = messageRef.current.querySelectorAll("pre > .hljs");
preElements.forEach((preElement) => {
const copyButton = document.createElement("button");
const copyImage = document.createElement("img");
copyImage.src = "/static/copy-button.svg";
copyImage.alt = "Copy";
copyImage.width = 24;
copyImage.height = 24;
copyButton.appendChild(copyImage);
copyButton.className = `hljs ${styles.codeCopyButton}`;
copyButton.addEventListener("click", () => {
let textContent = preElement.textContent || "";
// Strip any leading $ characters
textContent = textContent.replace(/^\$+/, "");
// Remove 'Copy' if it's at the start of the string
textContent = textContent.replace(/^Copy/, "");
textContent = textContent.trim();
navigator.clipboard.writeText(textContent);
copyImage.src = "/static/copy-button-success.svg";
});
preElement.prepend(copyButton);
});
console.log("render katex within the chat message");
renderMathInElement(messageRef.current, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "\\[", right: "\\]", display: true },
{ left: "$", right: "$", display: false },
{ left: "\\(", right: "\\)", display: false },
],
});
}
}, [markdownRendered, isHovering, messageRef]);
if (!props.chatMessage.message) {
return null;
}
function formatDate(timestamp: string) {
// Format date in HH:MM, DD MMM YYYY format
let date = new Date(timestamp + "Z");
let time_string = date
.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })
.toUpperCase();
let date_string = date
.toLocaleString("en-US", { year: "numeric", month: "short", day: "2-digit" })
.replaceAll("-", " ");
return `${time_string} on ${date_string}`;
}
function renderTimeStamp(timestamp: string) {
if (!timestamp.endsWith("Z")) {
timestamp = timestamp + "Z";
}
const messageDateTime = new Date(timestamp);
const currentDateTime = new Date();
const timeDiff = currentDateTime.getTime() - messageDateTime.getTime();
if (timeDiff < 60e3) {
return "Just now";
}
if (timeDiff < 3600e3) {
// Using Math.round for closer to actual time representation
return `${Math.round(timeDiff / 60e3)}m ago`;
}
if (timeDiff < 86400e3) {
return `${Math.round(timeDiff / 3600e3)}h ago`;
}
return `${Math.round(timeDiff / 86400e3)}d ago`;
}
function constructClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageContainer, "shadow-md"];
classes.push(styles[chatMessage.by]);
if (props.customClassName) {
classes.push(styles[`${chatMessage.by}${props.customClassName}`]);
}
return classes.join(" ");
}
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
let classes = [styles.chatMessageWrapper];
classes.push(styles[chatMessage.by]);
if (chatMessage.by === "khoj") {
classes.push(
`border-l-4 border-opacity-50 ${"border-l-" + props.borderLeftColor || "border-l-orange-400"}`,
);
}
return classes.join(" ");
}
async function playTextToSpeech() {
// Browser native speech API
// const utterance = new SpeechSynthesisUtterance(props.chatMessage.message);
// speechSynthesis.speak(utterance);
// Using the Khoj speech API
// Break the message up into chunks of sentences
const sentenceRegex = /[^.!?]+[.!?]*/g;
const chunks = props.chatMessage.message.match(sentenceRegex) || [];
if (!chunks) {
return;
}
if (chunks.length === 0) {
return;
}
if (!chunks[0]) {
return;
}
setIsPlaying(true);
let nextBlobPromise = fetchBlob(chunks[0]);
for (let i = 0; i < chunks.length; i++) {
if (interruptedRef.current) {
break; // Exit the loop if interrupted
}
const currentBlobPromise = nextBlobPromise;
if (i < chunks.length - 1) {
nextBlobPromise = fetchBlob(chunks[i + 1]);
}
try {
const blob = await currentBlobPromise;
const url = URL.createObjectURL(blob);
await playAudio(url);
} catch (error) {
console.error("Error:", error);
break; // Exit the loop on error
}
}
setIsPlaying(false);
setInterrupted(false); // Reset interrupted state after playback
}
async function fetchBlob(text: string) {
const response = await fetch(`/api/chat/speech?text=${encodeURIComponent(text)}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
return await response.blob();
}
function playAudio(url: string) {
return new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.onended = resolve;
audio.onerror = reject;
audio.play();
});
}
const allReferences = constructAllReferences(
props.chatMessage.context,
props.chatMessage.onlineContext,
);
return (
<div
className={`${styles.chatMessageContainer} ${styles[props.chatMessage.by]}`}
onClick={props.chatMessage.by === "khoj" ? (event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel) : undefined}>
{/* <div className={styles.chatFooter}> */}
{/* {props.chatMessage.by} */}
{/* </div> */}
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
{/* Add a copy button, thumbs up, and thumbs down buttons */}
<div className={styles.chatFooter}>
<div className={styles.chatTimestamp}>
{renderTimeStamp(props.chatMessage.created)}
</div>
<div className={styles.chatButtons}>
{
referencesValid &&
<div className={styles.referenceButton}>
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}>
References
</button>
</div>
}
<button className={`${styles.copyButton}`} onClick={() => {
navigator.clipboard.writeText(props.chatMessage.message);
setCopySuccess(true);
}}>
{
copySuccess ?
<Image
src="/copy-button-success.svg"
alt="Checkmark"
width={24}
height={24}
priority
className={constructClasses(props.chatMessage)}
onMouseLeave={(event) => setIsHovering(false)}
onMouseEnter={(event) => setIsHovering(true)}
onClick={props.chatMessage.by === "khoj" ? (event) => undefined : undefined}
>
<div className={chatMessageWrapperClasses(props.chatMessage)}>
<div
ref={messageRef}
className={styles.chatMessage}
dangerouslySetInnerHTML={{ __html: markdownRendered }}
/>
</div>
<div className={styles.teaserReferencesContainer}>
<TeaserReferencesSection
isMobileWidth={props.isMobileWidth}
notesReferenceCardData={allReferences.notesReferenceCardData}
onlineReferenceCardData={allReferences.onlineReferenceCardData}
/>
</div>
<div className={styles.chatFooter}>
{(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) && (
<>
<div
title={formatDate(props.chatMessage.created)}
className={`text-gray-400 relative top-0 left-4`}
>
{renderTimeStamp(props.chatMessage.created)}
</div>
<div className={`${styles.chatButtons} shadow-sm`}>
{props.chatMessage.by === "khoj" &&
(isPlaying ? (
interrupted ? (
<InlineLoading iconClassName="p-0" className="m-0" />
) : (
<button
title="Pause Speech"
onClick={(event) => setInterrupted(true)}
>
<Pause
alt="Pause Message"
color="hsl(var(--muted-foreground))"
/>
</button>
)
) : (
<button title="Speak" onClick={(event) => playTextToSpeech()}>
<SpeakerHigh
alt="Speak Message"
color="hsl(var(--muted-foreground))"
/>
</button>
))}
<button
title="Copy"
className={`${styles.copyButton}`}
onClick={() => {
navigator.clipboard.writeText(props.chatMessage.message);
setCopySuccess(true);
}}
>
{copySuccess ? (
<Copy alt="Copied Message" weight="fill" color="green" />
) : (
<Copy alt="Copy Message" color="hsl(var(--muted-foreground))" />
)}
</button>
{props.chatMessage.by === "khoj" &&
(props.chatMessage.intent ? (
<FeedbackButtons
uquery={props.chatMessage.intent.query}
kquery={props.chatMessage.message}
/>
: <Image
src="/copy-button.svg"
alt="Copy"
width={24}
height={24}
priority
) : (
<FeedbackButtons
uquery={
props.chatMessage.rawQuery || props.chatMessage.message
}
kquery={props.chatMessage.message}
/>
}
</button>
{
props.chatMessage.by === "khoj" && <FeedbackButtons />
}
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
)
);
}

View file

@ -0,0 +1,39 @@
import { CircleNotch } from "@phosphor-icons/react";
interface LoadingProps {
className?: string;
iconClassName?: string;
message?: string;
}
export default function Loading(props: LoadingProps) {
return (
// NOTE: We can display usage tips here for casual learning moments.
<div
className={
props.className ||
"bg-background opacity-50 flex items-center justify-center h-screen"
}
>
<div>
{props.message || "Loading"}{" "}
<span>
<CircleNotch className="inline animate-spin h-5 w-5" />
</span>
</div>
</div>
);
}
export function InlineLoading(props: LoadingProps) {
return (
<button className={`${props.className}`}>
<span>
{props.message}{" "}
<CircleNotch
className={`inline animate-spin ${props.iconClassName ? props.iconClassName : "h-5 w-5 mx-3"}`}
/>
</span>
</button>
);
}

View file

@ -0,0 +1,47 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import Link from "next/link";
export interface LoginPromptProps {
loginRedirectMessage: string;
onOpenChange: (open: boolean) => void;
}
export default function LoginPrompt(props: LoginPromptProps) {
return (
<AlertDialog open={true} onOpenChange={props.onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Sign in to Khoj to continue</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{props.loginRedirectMessage}. By logging in, you agree to our{" "}
<Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Dismiss</AlertDialogCancel>
<AlertDialogAction
className="bg-slate-400 hover:bg-slate-500"
onClick={() => {
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
}}
>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}>
{" "}
{/* Redirect to login page */}
Login
</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View file

@ -0,0 +1,191 @@
export function OrgMode({ className }: { className?: string }) {
const classes = className ?? "w-6 h-6 text-muted-foreground inline-flex mr-1";
return (
<svg
className={`${classes}`}
xmlns="http://www.w3.org/2000/svg"
width="144.98"
height="160"
viewBox="-7.65 -13.389 144.98 160"
>
<path
fill="#a04d32"
stroke="#000"
strokeWidth="3"
d="M133.399 46.067c-.205-3.15-2.842-4.366-5.993-2.125-7.22-1.297-14.305-.687-17.8-.981-7.662-1.073-14.041-5.128-14.041-5.128.932-1.239.486-3.917-5.498-4.101-1.646-.542-3.336-1.327-4.933-1.979.544-1.145-.133-2.836-.133-2.836 2.435-.672 2.808-3.842 1.848-5.709 3.106.084 2.612-4.718 2.183-6.381 2.435-.923 2.77-3.831 1.763-6.129 2.938-.671 3.022-4.114 2.77-6.548 3.023-.168 2.604-5.457 2.604-6.549 2.604-1.679 2.016-3.946 2.425-6.573 1.605-3.25-.577-4.173-2.116-.71-1.651 3.001-3.77 4.311-3.75 6.528.755 1.259-5.625 3.106-3.61 7.052-1.428 1.763-4.785 4.03-3.592 6.733-.606 1.326-4.888 4.433-3.041 7.371-4.03 2.687-3.79 3.335-2.938 5.793-1.147.736-2.318 1.862-2.995 3.094-1.32-1.568-2.603-4.429-2.584-8.294 0-3.275-6.1.318-6.1 6.784 0 .556-.056 1.061-.134 1.542-2.11.243-4.751.707-8.08 1.494-.106.073-.157.186-.182.316a8.704 8.704 0 01-.277-1.553c-.582-3.79-4.934-9.56-7.057-2.434-1.096 2.611-1.74 4.392-2.115 5.789v0s-.336.226-.957.61c-2.62 1.622-3.562 6.686-13.075 9.883-3.211 1.079-7.4 1.945-12.96 2.395-9.57.773-27.887 17.314-29.114 33.097-.283 3.964.31 13.737 3.596 22.31l.005.02c.015.042.032.081.048.122.052.134.103.267.156.398.28.718.579 1.405.895 2.062 1.885 4.028 4.46 7.59 7.934 9.882a25.252 25.252 0 004.372 2.762c5.907 9.749 18.442 22.252 42.075 14.859 36.255-10.284 56.263 13.809 58.568 15.5 3.399 3.433-8.786-29.835-34.587-44.788-15.253-8.322-5.678-22.656-4.585-27.718 0 0 12.227 8.557 21.087-4.52 8.004 2.062 13.367-1.462 20.25 1.03 4.184 1.833 21.77.726 15.235-9.104 4.11-2.683 4.544-1.815 6.6-5.9 1.104-4.952-1.403-6.012-2.167-7.366zM63.106 32.768c-.041.018-.086.04-.125.056.039-.034.075-.062.115-.102l.01.046zm-13.413 4.523c-.073.429-.143.829-.212 1.216.037-.832.085-1.714.143-2.646.024.435.05.904.069 1.43zm10.693-6.333c.746 1.124 1.662 2.179 1.662 2.179s-.875-.79-1.662-2.179z"
/>
<path
fill="#7a9"
stroke="#000"
strokeWidth=".5"
d="M6.448 104.253s10.02 36.105 46.549 24.68c36.255-10.284 56.263 13.809 58.568 15.5 3.399 3.433-8.786-29.835-34.587-44.788-15.253-8.322-5.678-22.656-4.585-27.718 0 0 12.227 8.557 21.087-4.52 8.004 2.062 13.367-1.462 20.25 1.03 4.184 1.833 21.77.726 15.235-9.104 4.11-2.683 4.544-1.815 6.6-5.9 1.105-4.952-1.402-6.011-2.166-7.366-.205-3.15-2.842-4.366-5.993-2.125-7.22-1.297-14.305-.687-17.8-.981-7.662-1.073-14.041-5.128-14.041-5.128.932-1.239.486-3.917-5.498-4.101-3.287-1.082-6.752-3.136-9.288-3.162-2.567 0-2.914-2.537-2.914-2.537-1.606-.87-3.924-4.252-3.9-9.438 0-3.275-6.098.318-6.098 6.784s-5.818 7.758-5.818 7.758-2.55-2.281-2.855-5.958c-.582-3.79-4.934-9.56-7.057-2.434-3.226 7.646-3.485 9.43-4.115 13.154-1.31 7.711-.345 8.012-.345 8.012l-32.824 23.43z"
/>
<path
fill="#314b49"
stroke="#314b49"
strokeWidth=".75"
strokeLinecap="round"
strokeLinejoin="round"
d="M84.11 42.833c1.549-.562.897-.415 1.153-.581-2.96.575-9.635.614-14.317-1.133.392.23 2.568.962 2.845 1.128.218.715.1 1.438 2.932 2.709 2.559.793 5.845.461 6.835-.529.109-1.684.126-1.065.553-1.594z"
/>
<path
fill="#314b49"
stroke="#314b49"
strokeWidth=".5"
d="M116.479 61.979c-2.83-2.085-4.881-.264-6.47-.413.99-.645 3.763-2.062 8.246-2.062 2.532 0 3.879 2.196 5.57 2.207 1.14.007 4.472-1.71 5.14-2.378-.97.838.454 1.755-.49 3.003-.281.359-.836 1.511-2.662 2.051-2.05.971-5.411 1.762-9.334-2.408z"
/>
<path
fill="#314b49"
d="M54.932 24.033s-3.355 7.996.312 15.329.522-6.829 4.688-4.162c3.397.385-2.387-3.215-2.033-7.819-.176-2.892-1.77-5.194-2.967-3.348zM119.336 50.417c0 1.121-1.363 2.03-3.045 2.03 3.573-1.121-.201-4.653-3.045-2.03 0-1.121 1.363-2.03 3.045-2.03s3.045.909 3.045 2.03z"
/>
<path
fill="#314b49"
d="M114.169 47.833c3.772-.231 6.336.323 5.536 3.138.548-1.126 1.292-2.83-1.046-3.507-1.746-.388-3.3-.378-4.49.369z"
/>
<path
fill="#a04d32"
stroke="#000"
strokeWidth=".5"
d="M59.929 69.234c0-3.521-1.51-7.166-7.04-14.583-1.635-2.192-2.62-4.336-3.211-6.275-1.401-3.295-3.426-8.019-.613-17.233 0 0 .62-.384 0 0-2.62 1.622-3.562 6.686-13.075 9.883-3.211 1.079-7.4 1.945-12.96 2.395-9.568.773-27.886 17.314-29.113 33.097-.283 3.964.31 13.737 3.596 22.31l.005.02c.015.042.032.081.048.122.052.134.103.267.156.398.28.718.579 1.405.895 2.062 1.885 4.028 4.46 7.59 7.934 9.882 3.084 2.404 5.606 3.306 5.606 3.306-2.588-3.578-3.77-7.562-2.263-12.32.65 2.637 1.903 4.162 3.646 4.777-.615-1.884-.827-3.549 0-4.651 2.567 6.734 5.353 9.031 8.17 10.686-2.63-4.914-4.031-10.005-3.77-15.337 2.569 6.028 6.596 9.945 10.56 13.954-3.78-5.966-6.911-12.104-6.977-19.046 1.693 2.778 3.935 4.932 6.6 6.601-1.683-2.709-2.505-5.51-2.263-8.423 4.424 4.945 9.36 6.607 14.332 8.046-5.197-3.625-9.843-7.537-12.32-12.572 2.972 1.464 5.948 1.693 8.926 1.383-3.706-1.872-5.07-5.252-5.783-9.052 5.177 5.279 10.587 8.827 16.09 11.692-5.455-5.26-9.478-10.65-11.565-16.218 2.1 1.18 4.157 1.736 6.16 1.509-2.766-3.124-3.465-6.182-4.211-9.241 2.637 3.916 4.959 6.022 7.103 7.103-2.19-4.482-2.034-8.432-.503-12.068 2.524 1.675 4.902 4.295 6.915 9.303.73-2.386-.447-6.364-1.886-10.56 2.175.622 4.779 3.351 8.17 9.932-.33-3.865-2.138-7.775-4.147-11.692 3.027 3.51 7.557 12.713 6.788 10.81z"
/>
<path
fill="#796958"
stroke="#000"
strokeWidth=".5"
d="M76.27 30.176s-.252 7.472 6.717 2.603c3.61.084 2.015-3.862 2.015-3.862 2.435-.672 2.808-3.842 1.848-5.709 3.106.084 2.612-4.718 2.183-6.381 2.435-.923 2.77-3.831 1.763-6.129 2.938-.671 3.022-4.114 2.77-6.548 3.023-.168 2.604-5.457 2.604-6.549 2.604-1.679 2.016-3.946 2.425-6.573 1.605-3.25-.577-4.173-2.116-.71-1.651 3.001-3.77 4.311-3.75 6.528.755 1.259-5.625 3.106-3.61 7.052-1.428 1.763-4.785 4.03-3.592 6.733-.606 1.326-4.888 4.433-3.041 7.371-4.03 2.687-3.79 3.335-2.938 5.793-2.155 1.38-4.41 4.131-3.278 6.381z"
/>
<path
fill="#fff"
d="M94.094-5.087s-.735 1.324-.735 2.133c0 .809 2.185.568 2.927-.227-1.625.024-2.965.289-2.192-1.906zM89.833 1.183s-.812 1.068-.183 2.316c.392.98 2.807.962 3.549.167-1.625.024-4.14-.287-3.366-2.483zM86.698 7.638s-.998 1.346-.492 2.602c0 .809 2.838.956 3.58.161-1.625.022-3.645-.489-3.088-2.763zM83.62 14.8s-1.402 1.542-.148 2.945c1.438.809 3.744.049 4.486-.746-1.625.022-4.894.076-4.337-2.199zM80.616 20.521s-1.575 1.414-.02 3.312c1.438.809 4.57.198 5.312-.597-1.624.024-5.99-1.346-5.292-2.715zM77.469 27.262s-1.403 1.542-.15 2.945c1.439.809 6.037-.186 6.779-.981-1.625.023-7.185.311-6.63-1.964z"
/>
<path
fill="#a04d32"
stroke="#000"
strokeWidth=".5"
d="M76.352 30.29c-.45-.45-.534-.896-.367-1.718 0 0 .369-4.107-16.333-.158-1.072.74 2.396 4.722 2.396 4.722s.418.215 1.047-.415c.253 1.123.852 4.081.233 4.579 1.245-.771 1.868-1.946 1.676-4.125 2.122.461 3.742 1.64 4.692 3.779.304-1.4.603-2.799-.384-4.126 2.182.285 3.88 1.496 5.362 3.124.22-.933.354-1.883 0-2.931 1.39.473 2.587 1.607 3.71 2.988 0 0 .21-3.862-2.032-5.719z"
/>
<path
opacity=".26"
d="M60.33 57.936s6.76 13.59 17.595 13.991c10.834.401 10.834-2.73 10.834-2.73s-15.527 3.048-28.43-11.261zM63.485 65.63c2.279 3.104 4.856 5.221 7.722 6.382 0 0-7.365 11.108-3.611 20.023s13.125 11.053 23.32 21.249c7.943 7.942 17.159 24.961 17.159 24.961s-17.834-14.176-29.42-13.479c0 0-2.687-9.668-10.585-17.566-11.244-11.245-16.168-25.875-4.585-41.57z"
/>
<path
opacity=".18"
fill="#fff"
d="M44.717 38.245c-3.874 2.501-8.42 7.096-24.415 8.083C3.252 53.112-7.131 73.013-3.475 86.792c1.348 7.317 3.89 14.18 3.89 14.18C-.47 95.053-.966 89.575 1.45 87.373c1.435 2.384 2.969 2.468 4.507 2.479-1.59-2.404-1.788-4.808 0-7.212 1.489 1.525 2.992 1.881 4.507 1.353-2.128-2.449-1.867-4.848 0-7.211 1.388 5.022 4.462 7.453 7.662 9.689-2.208-4.333-4.166-8.672-2.93-13.07 1.323.729 2.595.644 3.83 0-1.256-1.576-.924-3.153 0-4.732 2.948 3.04 6.214 3.724 9.466 4.507-2.661-2.454-5.543-4.527-6.761-9.465 1.5-1.811 3.269-2.685 5.408-2.253-1.901-1.167-1.65-2.543 0-4.057 2.089 1.104 4.195 1.352 6.31 1.127-2.807-1.68-8.424-4.994 11.269-20.283z"
/>
<path
opacity=".27"
fill="#fff"
d="M71.278 19.389c.996-.963 1.146-.65.854.285-.982 2.36.353 4.647.797 6.206l-3.871.114c.108-2.271.247-4.794 2.22-6.605z"
/>
</svg>
);
}
export function Markdown({ className }: { className?: string }) {
const classes = className ?? "w-6 h-6 text-muted-foreground inline-flex mr-1";
return (
<svg
className={`${classes}`}
xmlns="http://www.w3.org/2000/svg"
width="208"
height="128"
viewBox="0 0 208 128"
>
<rect
width="198"
height="118"
x="5"
y="5"
ry="10"
stroke="#000"
strokeWidth="10"
fill="none"
/>
<path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z" />
</svg>
);
}
export function Pdf({ className }: { className?: string }) {
const classes = className ?? "w-6 h-6 text-muted-foreground inline-flex mr-1";
return (
<svg
className={`${classes}`}
xmlns="http://www.w3.org/2000/svg"
enableBackground="new 0 0 334.371 380.563"
version="1.1"
viewBox="0 0 14 16"
>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
<polygon
points="51.791 356.65 51.791 23.99 204.5 23.99 282.65 102.07 282.65 356.65"
fill="#fff"
strokeWidth="212.65"
/>
<path
d="m201.19 31.99 73.46 73.393v243.26h-214.86v-316.66h141.4m6.623-16h-164.02v348.66h246.85v-265.9z"
strokeWidth="21.791"
/>
</g>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
<polygon
points="282.65 356.65 51.791 356.65 51.791 23.99 204.5 23.99 206.31 25.8 206.31 100.33 280.9 100.33 282.65 102.07"
fill="#fff"
strokeWidth="212.65"
/>
<path
d="m198.31 31.99v76.337h76.337v240.32h-214.86v-316.66h138.52m9.5-16h-164.02v348.66h246.85v-265.9l-6.43-6.424h-69.907v-69.842z"
strokeWidth="21.791"
/>
</g>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" strokeWidth="21.791">
<polygon points="258.31 87.75 219.64 87.75 219.64 48.667 258.31 86.38" />
<path d="m227.64 67.646 12.41 12.104h-12.41v-12.104m-5.002-27.229h-10.998v55.333h54.666v-12.742z" />
</g>
<g
transform="matrix(.04589 0 0 .04589 -.66877 -.73379)"
fill="#ed1c24"
strokeWidth="212.65"
>
<polygon points="311.89 284.49 22.544 284.49 22.544 167.68 37.291 152.94 37.291 171.49 297.15 171.49 297.15 152.94 311.89 167.68" />
<path d="m303.65 168.63 1.747 1.747v107.62h-276.35v-107.62l1.747-1.747v9.362h272.85v-9.362m-12.999-31.385v27.747h-246.86v-27.747l-27.747 27.747v126h302.35v-126z" />
</g>
<rect x="1.7219" y="7.9544" width="10.684" height="4.0307" fill="none" />
<g transform="matrix(.04589 0 0 .04589 1.7219 11.733)" fill="#fff" strokeWidth="21.791">
<path d="m9.216 0v-83.2h30.464q6.784 0 12.928 1.408 6.144 1.28 10.752 4.608 4.608 3.2 7.296 8.576 2.816 5.248 2.816 13.056 0 7.68-2.816 13.184-2.688 5.504-7.296 9.088-4.608 3.456-10.624 5.248-6.016 1.664-12.544 1.664h-8.96v26.368zm22.016-43.776h7.936q6.528 0 9.6-3.072 3.2-3.072 3.2-8.704t-3.456-7.936-9.856-2.304h-7.424z" />
<path d="m87.04 0v-83.2h24.576q9.472 0 17.28 2.304 7.936 2.304 13.568 7.296t8.704 12.8q3.2 7.808 3.2 18.816t-3.072 18.944-8.704 13.056q-5.504 5.12-13.184 7.552-7.552 2.432-16.512 2.432zm22.016-17.664h1.28q4.48 0 8.448-1.024 3.968-1.152 6.784-3.84 2.944-2.688 4.608-7.424t1.664-12.032-1.664-11.904-4.608-7.168q-2.816-2.56-6.784-3.456-3.968-1.024-8.448-1.024h-1.28z" />
<path d="m169.22 0v-83.2h54.272v18.432h-32.256v15.872h27.648v18.432h-27.648v30.464z" />
</g>
</svg>
);
}
export function Word({ className }: { className?: string }) {
const classes = className ?? "w-6 h-6 text-muted-foreground inline-flex mr-1";
return (
<svg
className={`${classes}`}
xmlns="http://www.w3.org/2000/svg"
fill="#FFF"
stroke-miterlimit="10"
strokeWidth="2"
viewBox="0 0 96 96"
>
<path
stroke="#979593"
d="M67.1716 7H27c-1.1046 0-2 .8954-2 2v78c0 1.1046.8954 2 2 2h58c1.1046 0 2-.8954 2-2V26.8284c0-.5304-.2107-1.0391-.5858-1.4142L68.5858 7.5858C68.2107 7.2107 67.702 7 67.1716 7z"
/>
<path fill="none" stroke="#979593" d="M67 7v18c0 1.1046.8954 2 2 2h18" />
<path
fill="#C8C6C4"
d="M79 61H48v-2h31c.5523 0 1 .4477 1 1s-.4477 1-1 1zm0-6H48v-2h31c.5523 0 1 .4477 1 1s-.4477 1-1 1zm0-6H48v-2h31c.5523 0 1 .4477 1 1s-.4477 1-1 1zm0-6H48v-2h31c.5523 0 1 .4477 1 1s-.4477 1-1 1zm0 24H48v-2h31c.5523 0 1 .4477 1 1s-.4477 1-1 1z"
/>
<path
fill="#185ABD"
d="M12 74h32c2.2091 0 4-1.7909 4-4V38c0-2.2091-1.7909-4-4-4H12c-2.2091 0-4 1.7909-4 4v32c0 2.2091 1.7909 4 4 4z"
/>
<path d="M21.6245 60.6455c.0661.522.109.9769.1296 1.3657h.0762c.0306-.3685.0889-.8129.1751-1.3349.0862-.5211.1703-.961.2517-1.319L25.7911 44h4.5702l3.6562 15.1272c.183.7468.3353 1.6973.457 2.8532h.0608c.0508-.7979.1777-1.7184.3809-2.7615L37.8413 44H42l-5.1183 22h-4.86l-3.4885-14.5744c-.1016-.4197-.2158-.9663-.3428-1.6417-.127-.6745-.2057-1.1656-.236-1.4724h-.0608c-.0407.358-.1195.8896-.2364 1.595-.1169.7062-.211 1.2273-.2819 1.565L24.1 66h-4.9357L14 44h4.2349l3.1843 15.3882c.0709.3165.1392.7362.2053 1.2573z" />
</svg>
);
}

View file

@ -0,0 +1,89 @@
export function KhojLogoType() {
return (
<svg
width="70"
height="auto"
viewBox="0 0 442 200"
className="fill-zinc-950 dark:fill-zinc-300"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_45_75)">
<path
d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z"
fill="#FAE80B"
/>
<path
d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z"
fill="#FFCC09"
/>
<path
d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z"
fill="#FBA719"
/>
<path d="M217.018 30.3459C218.946 32.2774 217.886 40.2931 217.886 45.315C217.886 48.4054 218.368 51.0129 218.368 53.4273C218.368 56.0348 217.886 58.5458 217.886 60.9602C218.368 62.9882 217.886 70.6177 218.946 71.1006C220.489 72.1629 222.031 66.9478 222.513 66.465C224.056 62.9883 222.995 65.1129 224.923 60.9602C226.948 57.0006 229.069 52.8479 230.611 49.9506C231.961 46.8602 234.082 42.4177 235.624 38.7479C237.167 36.3335 238.227 31.6979 240.637 29.7664C242.18 28.2212 247.193 28.2212 251.821 28.2212C255.291 28.7041 258.858 28.7041 259.919 30.6356C261.268 33.726 255.773 41.8383 254.231 45.2184C252.303 49.3712 250.76 51.8821 248.736 55.3588C248.254 56.4211 248.254 57.3869 247.675 58.9321C245.747 63.954 243.24 67.3341 240.637 72.5492C239.577 74.4807 238.709 76.9917 237.167 79.1163C236.106 82.0136 234.564 84.6211 234.564 86.6492C234.564 89.5464 236.588 93.6992 237.649 95.6307C241.12 102.294 244.687 108.861 248.157 115.911C248.639 117.264 248.639 118.809 249.7 120.354C251.242 124.313 253.267 127.404 255.677 132.426C257.219 135.999 261.172 142.566 260.304 145.463C258.762 149.037 254.616 148.071 250.182 148.071C244.687 148.071 240.541 148.071 238.709 147.009C236.685 145.463 235.142 139.959 233.6 137.061C230.129 128.949 228.587 124.313 224.634 115.911C223.574 112.821 222.031 109.731 220.489 107.316C218.464 104.709 218.464 112.821 217.886 116.394C217.886 120.837 217.886 124.313 217.886 126.342C217.886 132.909 219.428 144.015 216.536 147.009C213.451 149.616 198.894 149.037 196.773 145.463C196.291 143.532 196.773 137.544 196.773 132.426C197.255 127.404 196.773 124.313 196.773 119.871C196.291 108.861 196.291 98.7211 196.291 87.7115C196.291 83.5588 197.351 79.5992 196.773 76.5088C196.773 74.4807 196.291 73.0321 196.291 71.004C196.291 69.4588 196.773 68.3965 196.773 66.8513C196.773 63.3746 196.291 59.8013 196.291 56.3246C196.291 52.7513 196.773 49.2746 196.773 46.1842C196.773 41.7417 195.423 36.237 196.291 32.5671C196.773 31.0219 198.315 29.6698 198.894 29.0904C199.376 29.0904 199.376 29.0904 199.954 28.6075C203.811 27.2554 214.994 27.7384 217.018 30.3459Z" />
<path d="M327.788 29.863C328.849 30.3459 328.849 32.2774 329.331 34.3054C329.813 38.265 329.331 46.3773 329.331 51.0129C328.849 58.5458 329.331 66.465 329.331 72.1629C329.331 82.1102 329.331 97.2724 329.331 105.868C329.331 114.946 329.331 127.5 328.849 138.22C328.849 142.663 329.331 146.14 327.306 147.685C324.896 149.23 310.628 149.23 308.796 147.685C306.772 145.657 307.736 139.09 307.736 134.647C307.736 132.619 307.736 130.687 307.736 128.659C307.736 119.002 307.254 109.924 307.736 98.9143C307.736 96.3067 308.218 93.4094 307.736 92.3471C306.193 90.8019 302.241 91.8642 299.156 91.8642C297.131 91.8642 292.6 90.319 290.576 92.3471C288.648 94.9546 289.515 102.005 289.515 106.447C289.515 117.457 289.997 123.155 289.997 134.647C289.997 140.635 290.479 145.657 287.587 147.685C284.502 149.713 272.451 149.23 270.427 147.202C267.342 144.305 267.824 132.619 267.824 125.569C267.824 103.936 268.884 87.9047 268.306 68.5896C268.306 64.63 268.788 60.4773 268.788 57.58C268.788 55.552 268.306 53.4273 267.728 50.5301C267.728 48.9849 268.21 46.9568 268.21 45.4116C268.692 39.9068 267.728 32.8568 268.692 30.8288C270.716 27.9315 286.334 27.352 288.744 30.4424C290.768 33.3397 289.226 39.4239 289.226 42.9972C288.744 46.9568 289.226 52.0752 289.226 57.58C289.226 59.1252 289.226 62.0225 289.226 64.6301C289.226 67.0444 289.226 70.6177 289.708 71.68C290.768 73.2252 295.878 73.2252 298.288 73.2252C300.313 73.2252 305.808 73.7081 306.868 72.1629C307.929 70.6177 307.929 62.0225 307.929 59.1253C307.929 51.013 307.929 46.3773 307.929 38.265C307.929 35.3677 307.447 32.2774 308.411 30.3459C309.471 28.3178 311.881 28.8006 313.906 28.3178C315.737 28.3178 325.764 27.2555 327.788 29.863Z" />
<path d="M371.652 26.1931C379.75 25.7102 387.752 28.2212 392.765 30.6356C394.308 31.6979 397.778 35.2712 398.742 36.8164C401.345 40.2931 402.888 42.8041 403.755 47.3431C404.237 48.4054 404.816 48.8883 404.816 49.3711C404.816 49.854 404.816 50.4335 404.816 51.3993C405.298 53.8136 406.358 56.4211 406.84 57.9664C406.84 59.8979 406.84 61.4431 406.84 63.4711C406.84 65.0163 407.322 66.3684 407.322 67.9136C408.383 76.9916 407.804 87.7115 407.322 97.6587C407.322 104.226 407.322 110.696 406.262 116.781C405.201 122.961 403.659 129.529 402.309 132.909C401.827 133.971 400.285 136.482 399.224 137.544L398.742 138.027C397.2 140.442 394.308 142.47 391.223 144.594C389.68 145.463 389.198 145.946 387.752 146.526C386.21 147.009 384.667 147.009 382.064 147.588C380.714 147.588 379.172 148.65 377.629 148.65C374.159 149.133 369.531 149.133 367.121 149.133C364.036 148.65 360.469 147.588 357.481 146.043C356.999 146.043 356.42 145.174 356.42 145.174C354.878 144.691 352.853 143.629 351.986 143.146C349.961 141.6 347.358 138.51 345.816 136.579C342.345 130.591 339.838 121.416 339.26 112.821C338.778 106.254 338.778 97.6588 338.778 92.154C338.296 88.0012 338.778 83.5588 339.26 79.5992C339.26 73.9013 339.26 67.9136 339.742 62.8917C340.224 60.2841 340.803 57.8698 341.285 55.8417C341.285 52.7513 341.285 50.3369 341.767 47.7294C343.309 43.287 346.78 35.6575 350.347 32.5671C351.407 32.0842 352.757 31.0219 354.299 30.1527C358.445 28.1246 364.422 26.5795 370.978 26H371.652V26.1931ZM361.53 59.8979C359.987 72.6458 360.469 86.1663 361.048 99.3005C361.53 109.441 362.108 124.893 366.736 128.563C366.736 129.046 368.085 129.625 369.146 130.108C373.291 131.46 376.665 130.591 379.75 128.563C380.811 127.5 382.161 125.955 382.643 124.99C384.185 122.575 385.245 115.525 385.728 110.89C385.728 106.93 385.728 101.908 386.21 96.3067C386.21 85.2971 385.728 74.0944 384.667 64.6301C384.185 58.0629 383.607 52.0753 381.582 48.9849C380.232 46.9568 378.111 45.4116 376.087 44.8322C373.677 43.963 372.616 44.8322 371.652 44.8322H371.17C364.615 45.7979 362.59 52.365 361.53 59.8979Z" />
<path d="M439.426 28.2212C441.836 30.6356 441.45 89.4499 441.45 96.9827C440.968 113.497 440.968 125.183 440.968 142.759C440.968 145.85 441.45 147.395 441.45 149.423C441.45 153.383 440.39 159.37 440.968 165.454C440.968 171.442 441.45 178.878 439.908 182.259C438.847 186.701 434.798 191.916 430.846 194.33C426.893 196.938 420.241 197.904 412.336 197.421C407.226 196.938 401.153 196.359 399.128 193.268C398.646 191.916 398.646 187.763 398.646 184.866C398.646 180.713 398.646 177.816 400.189 176.754C403.274 175.209 409.829 180.23 414.746 178.782C416.77 178.299 418.891 176.174 419.855 172.601C420.241 169.704 419.855 165.455 419.855 161.978C419.855 158.887 420.241 156.473 420.241 153.383C420.241 147.395 420.241 140.828 420.241 134.84C420.241 127.307 419.373 120.257 419.373 113.497C419.373 104.033 419.855 47.3431 419.855 38.7479C419.855 32.7602 417.831 28.8006 422.266 26.676C425.833 25.6136 436.823 25.6137 439.426 28.2212Z" />
<path d="M46.6374 143.679C43.5946 143.679 41.2864 142.838 39.6601 141.104C36.7223 138.004 37.5092 133.379 37.5617 133.169C37.719 132.275 52.3031 43.8816 53.667 35.2104C54.9261 27.1698 64.0542 25.383 68.7232 25.5932L68.6183 28.7464C68.5658 28.7464 65.733 28.6413 62.8476 29.5347C59.2803 30.6383 57.2344 32.6878 56.7622 35.6834C55.4507 44.3546 40.8142 132.801 40.6568 133.694C40.6568 133.747 40.1322 137.005 41.9684 138.95C43.2799 140.316 45.4832 140.789 48.6308 140.368L48.9981 143.469C48.2112 143.627 47.4243 143.679 46.6374 143.679Z" />
<path d="M106.023 33.371H102.928C102.98 33.2133 102.98 33.0031 102.98 32.8455V15.2403C102.98 13.506 101.564 12.0871 99.8324 12.0871H89.4977C90.8616 10.8784 91.7535 9.09161 91.7535 7.09461C91.7535 3.41592 88.7632 0.42041 85.091 0.42041C81.4188 0.42041 78.4285 3.41592 78.4285 7.09461C78.4285 9.09161 79.2679 10.8784 80.6843 12.0871H70.8217C69.0905 12.0871 67.6741 13.506 67.6741 15.2403V32.8455C67.6741 33.0557 67.6741 33.2133 67.7265 33.371H64.0543C61.5887 33.371 59.5427 35.4205 59.5427 37.8905C59.5427 40.4131 61.5887 42.4101 64.0543 42.4101H106.075C108.541 42.4101 110.587 40.3605 110.587 37.8905C110.534 35.368 108.541 33.371 106.023 33.371ZM85.0385 3.52103C86.9795 3.52103 88.5534 5.0976 88.5534 7.04205C88.5534 8.98651 86.9795 10.5631 85.0385 10.5631C83.0975 10.5631 81.5237 8.98651 81.5237 7.04205C81.5237 5.0976 83.0975 3.52103 85.0385 3.52103Z" />
<path d="M123.177 143.679C122.443 143.679 121.656 143.627 120.816 143.522L121.184 140.421C124.331 140.841 126.535 140.316 127.846 139.002C129.682 137.058 129.158 133.799 129.158 133.799C129 132.906 114.364 44.4596 113.052 35.7884C112.58 32.7929 110.534 30.7433 106.967 29.6397C104.082 28.7463 101.249 28.8514 101.196 28.8514L101.091 25.6983C105.76 25.4881 114.941 27.2749 116.147 35.3154C117.459 43.9866 132.095 132.38 132.253 133.274C132.305 133.431 133.092 138.056 130.154 141.157C128.528 142.786 126.22 143.679 123.177 143.679Z" />
<path d="M122.443 151.142L122.39 129.805C122.39 124.76 109.642 120.031 109.642 120.031H109.433C105.026 124.34 97.5765 127.44 85.2483 127.44C72.9201 127.44 65.5231 124.34 61.064 120.031H60.8541C60.8541 120.031 48.1063 124.76 48.1063 129.805C48.1063 134.85 48.1063 151.142 48.1063 151.142C48.1063 151.142 42.1782 154.978 42.1782 159.445C42.1782 163.912 42.1782 167.17 42.1782 167.17C42.1782 167.17 47.2144 178.154 85.2483 178.154H85.3007C123.335 178.154 128.371 167.17 128.371 167.17C128.371 167.17 128.371 163.912 128.371 159.445C128.371 154.978 122.443 151.142 122.443 151.142Z" />
<path d="M117.931 87.658C116.725 85.2406 113.42 80.5108 112.056 78.5664L103.924 41.1488L101.091 41.7795L112.738 95.2256L112.79 95.3833C112.79 95.4358 113.262 96.8547 113.63 99.062C113.052 99.5875 112.37 100.06 111.584 100.533C108.331 102.478 96.7897 105.894 85.3009 108.994C73.8645 105.894 62.3756 102.478 59.1231 100.533C58.2837 100.008 57.5493 99.4824 56.9197 98.9043C57.287 96.7496 57.7066 95.3833 57.7591 95.3307V95.2782L69.4053 41.5167L66.5725 40.886L58.3362 79.0394C56.8148 81.194 53.877 85.4508 52.7754 87.7106C51.8311 89.6025 50.7819 94.5424 54.0344 98.8518C53.2475 103.739 52.9852 111.78 57.7591 118.401C62.7953 125.391 72.0284 128.912 85.2484 128.912C98.4685 128.912 107.702 125.391 112.738 118.401C117.459 111.832 117.249 103.949 116.515 99.062C120.03 94.6475 118.928 89.6025 117.931 87.658ZM116.095 88.604C116.725 89.9178 117.459 92.9658 115.99 96.0139C115.78 95.2256 115.623 94.7526 115.571 94.5424L113.262 83.9793C114.364 85.661 115.518 87.4478 116.095 88.604ZM54.664 88.604C55.1886 87.5529 56.1328 86.0289 57.182 84.4523L54.9787 94.4899C54.9262 94.6475 54.7689 95.068 54.6115 95.6986C53.4049 92.7556 54.0869 89.8652 54.664 88.604ZM56.6574 101.374C57.0771 101.69 57.6017 102.005 58.0739 102.32C61.1166 104.16 70.9792 107.155 81.3139 110.045C70.5595 112.936 60.8543 115.301 59.4378 115.669C56.5525 110.939 56.2902 105.473 56.6574 101.374ZM85.3009 125.969C73.6546 125.969 65.4184 123.131 60.8018 117.508C64.1068 116.72 74.5465 114.092 85.3533 111.149C95.9504 114.039 106.338 116.615 109.8 117.508C105.183 123.131 96.9471 125.969 85.3009 125.969ZM111.111 115.669C109.17 115.196 99.7275 112.831 89.3403 110.045C99.7275 107.155 109.59 104.107 112.685 102.268C113.157 102.005 113.525 101.742 113.944 101.427C114.311 105.526 113.997 110.991 111.111 115.669Z" />
<path d="M39.188 44.7224C38.8733 44.7224 38.611 44.6173 38.3487 44.4597L24.2893 34.5797C23.6073 34.1068 23.4499 33.2134 23.922 32.5302C24.3942 31.847 25.286 31.6893 25.968 32.1623L40.0274 42.0422C40.7094 42.5152 40.8668 43.4086 40.3946 44.0918C40.0799 44.5122 39.6602 44.7224 39.188 44.7224Z" />
<path d="M18.8334 80.6685L1.67885 80.6159C0.891941 80.6159 0.209961 79.9853 0.209961 79.1445C0.209961 78.3036 0.83948 77.673 1.67885 77.673L18.8334 77.7255C19.6203 77.7255 20.3023 78.3562 20.3023 79.197C20.3023 80.0379 19.6203 80.6685 18.8334 80.6685Z" />
<path d="M13.2726 128.754C12.8004 128.754 12.3283 128.544 12.066 128.071C11.6463 127.388 11.8562 126.495 12.4857 126.074L26.9123 116.825C27.5943 116.404 28.4861 116.562 28.9058 117.245C29.3255 117.928 29.1157 118.822 28.4861 119.242L14.0595 128.492C13.8497 128.649 13.5874 128.754 13.2726 128.754Z" />
<path d="M130.889 43.6188C130.417 43.6188 129.997 43.4086 129.682 42.9881C129.21 42.305 129.368 41.4116 130.05 40.9386L144.109 31.0587C144.738 30.5857 145.683 30.7433 146.155 31.4265C146.627 32.1097 146.47 33.0031 145.788 33.4761L131.728 43.356C131.466 43.5137 131.204 43.6188 130.889 43.6188Z" />
<path d="M151.244 79.5649C150.457 79.5649 149.775 78.9343 149.775 78.0934C149.775 77.3051 150.404 76.6219 151.244 76.6219L168.398 76.5694C169.185 76.5694 169.867 77.2 169.867 78.0409C169.867 78.8292 169.238 79.5123 168.398 79.5123L151.244 79.5649Z" />
<path d="M156.804 127.598C156.542 127.598 156.28 127.546 156.017 127.388L141.591 118.139C140.909 117.718 140.699 116.825 141.171 116.142C141.591 115.458 142.483 115.248 143.165 115.721L157.591 124.97C158.273 125.391 158.483 126.284 158.011 126.967C157.749 127.388 157.277 127.598 156.804 127.598Z" />
</g>
<defs>
<clipPath id="clip0_45_75">
<rect width="442" height="200" fill="currentColor" />
</clipPath>
</defs>
</svg>
);
}
export function KhojLogo() {
return (
<svg
width="200"
height="200"
viewBox="0 0 200 200"
className="fill-zinc-950 dark:fill-zinc-300"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_45_75)">
<path
d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z"
fill="#FAE80B"
/>
<path
d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z"
fill="#FFCC09"
/>
<path
d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z"
fill="#FBA719"
/>
<path d="M46.6374 143.679C43.5946 143.679 41.2864 142.838 39.6601 141.104C36.7223 138.004 37.5092 133.379 37.5617 133.169C37.719 132.275 52.3031 43.8816 53.667 35.2104C54.9261 27.1698 64.0542 25.383 68.7232 25.5932L68.6183 28.7464C68.5658 28.7464 65.733 28.6413 62.8476 29.5347C59.2803 30.6383 57.2344 32.6878 56.7622 35.6834C55.4507 44.3546 40.8142 132.801 40.6568 133.694C40.6568 133.747 40.1322 137.005 41.9684 138.95C43.2799 140.316 45.4832 140.789 48.6308 140.368L48.9981 143.469C48.2112 143.627 47.4243 143.679 46.6374 143.679Z" />
<path d="M106.023 33.371H102.928C102.98 33.2133 102.98 33.0031 102.98 32.8455V15.2403C102.98 13.506 101.564 12.0871 99.8324 12.0871H89.4977C90.8616 10.8784 91.7535 9.09161 91.7535 7.09461C91.7535 3.41592 88.7632 0.42041 85.091 0.42041C81.4188 0.42041 78.4285 3.41592 78.4285 7.09461C78.4285 9.09161 79.2679 10.8784 80.6843 12.0871H70.8217C69.0905 12.0871 67.6741 13.506 67.6741 15.2403V32.8455C67.6741 33.0557 67.6741 33.2133 67.7265 33.371H64.0543C61.5887 33.371 59.5427 35.4205 59.5427 37.8905C59.5427 40.4131 61.5887 42.4101 64.0543 42.4101H106.075C108.541 42.4101 110.587 40.3605 110.587 37.8905C110.534 35.368 108.541 33.371 106.023 33.371ZM85.0385 3.52103C86.9795 3.52103 88.5534 5.0976 88.5534 7.04205C88.5534 8.98651 86.9795 10.5631 85.0385 10.5631C83.0975 10.5631 81.5237 8.98651 81.5237 7.04205C81.5237 5.0976 83.0975 3.52103 85.0385 3.52103Z" />
<path d="M123.177 143.679C122.443 143.679 121.656 143.627 120.816 143.522L121.184 140.421C124.331 140.841 126.535 140.316 127.846 139.002C129.682 137.058 129.158 133.799 129.158 133.799C129 132.906 114.364 44.4596 113.052 35.7884C112.58 32.7929 110.534 30.7433 106.967 29.6397C104.082 28.7463 101.249 28.8514 101.196 28.8514L101.091 25.6983C105.76 25.4881 114.941 27.2749 116.147 35.3154C117.459 43.9866 132.095 132.38 132.253 133.274C132.305 133.431 133.092 138.056 130.154 141.157C128.528 142.786 126.22 143.679 123.177 143.679Z" />
<path d="M122.443 151.142L122.39 129.805C122.39 124.76 109.642 120.031 109.642 120.031H109.433C105.026 124.34 97.5765 127.44 85.2483 127.44C72.9201 127.44 65.5231 124.34 61.064 120.031H60.8541C60.8541 120.031 48.1063 124.76 48.1063 129.805C48.1063 134.85 48.1063 151.142 48.1063 151.142C48.1063 151.142 42.1782 154.978 42.1782 159.445C42.1782 163.912 42.1782 167.17 42.1782 167.17C42.1782 167.17 47.2144 178.154 85.2483 178.154H85.3007C123.335 178.154 128.371 167.17 128.371 167.17C128.371 167.17 128.371 163.912 128.371 159.445C128.371 154.978 122.443 151.142 122.443 151.142Z" />
<path d="M117.931 87.658C116.725 85.2406 113.42 80.5108 112.056 78.5664L103.924 41.1488L101.091 41.7795L112.738 95.2256L112.79 95.3833C112.79 95.4358 113.262 96.8547 113.63 99.062C113.052 99.5875 112.37 100.06 111.584 100.533C108.331 102.478 96.7897 105.894 85.3009 108.994C73.8645 105.894 62.3756 102.478 59.1231 100.533C58.2837 100.008 57.5493 99.4824 56.9197 98.9043C57.287 96.7496 57.7066 95.3833 57.7591 95.3307V95.2782L69.4053 41.5167L66.5725 40.886L58.3362 79.0394C56.8148 81.194 53.877 85.4508 52.7754 87.7106C51.8311 89.6025 50.7819 94.5424 54.0344 98.8518C53.2475 103.739 52.9852 111.78 57.7591 118.401C62.7953 125.391 72.0284 128.912 85.2484 128.912C98.4685 128.912 107.702 125.391 112.738 118.401C117.459 111.832 117.249 103.949 116.515 99.062C120.03 94.6475 118.928 89.6025 117.931 87.658ZM116.095 88.604C116.725 89.9178 117.459 92.9658 115.99 96.0139C115.78 95.2256 115.623 94.7526 115.571 94.5424L113.262 83.9793C114.364 85.661 115.518 87.4478 116.095 88.604ZM54.664 88.604C55.1886 87.5529 56.1328 86.0289 57.182 84.4523L54.9787 94.4899C54.9262 94.6475 54.7689 95.068 54.6115 95.6986C53.4049 92.7556 54.0869 89.8652 54.664 88.604ZM56.6574 101.374C57.0771 101.69 57.6017 102.005 58.0739 102.32C61.1166 104.16 70.9792 107.155 81.3139 110.045C70.5595 112.936 60.8543 115.301 59.4378 115.669C56.5525 110.939 56.2902 105.473 56.6574 101.374ZM85.3009 125.969C73.6546 125.969 65.4184 123.131 60.8018 117.508C64.1068 116.72 74.5465 114.092 85.3533 111.149C95.9504 114.039 106.338 116.615 109.8 117.508C105.183 123.131 96.9471 125.969 85.3009 125.969ZM111.111 115.669C109.17 115.196 99.7275 112.831 89.3403 110.045C99.7275 107.155 109.59 104.107 112.685 102.268C113.157 102.005 113.525 101.742 113.944 101.427C114.311 105.526 113.997 110.991 111.111 115.669Z" />
<path d="M39.188 44.7224C38.8733 44.7224 38.611 44.6173 38.3487 44.4597L24.2893 34.5797C23.6073 34.1068 23.4499 33.2134 23.922 32.5302C24.3942 31.847 25.286 31.6893 25.968 32.1623L40.0274 42.0422C40.7094 42.5152 40.8668 43.4086 40.3946 44.0918C40.0799 44.5122 39.6602 44.7224 39.188 44.7224Z" />
<path d="M18.8334 80.6685L1.67885 80.6159C0.891941 80.6159 0.209961 79.9853 0.209961 79.1445C0.209961 78.3036 0.83948 77.673 1.67885 77.673L18.8334 77.7255C19.6203 77.7255 20.3023 78.3562 20.3023 79.197C20.3023 80.0379 19.6203 80.6685 18.8334 80.6685Z" />
<path d="M13.2726 128.754C12.8004 128.754 12.3283 128.544 12.066 128.071C11.6463 127.388 11.8562 126.495 12.4857 126.074L26.9123 116.825C27.5943 116.404 28.4861 116.562 28.9058 117.245C29.3255 117.928 29.1157 118.822 28.4861 119.242L14.0595 128.492C13.8497 128.649 13.5874 128.754 13.2726 128.754Z" />
<path d="M130.889 43.6188C130.417 43.6188 129.997 43.4086 129.682 42.9881C129.21 42.305 129.368 41.4116 130.05 40.9386L144.109 31.0587C144.738 30.5857 145.683 30.7433 146.155 31.4265C146.627 32.1097 146.47 33.0031 145.788 33.4761L131.728 43.356C131.466 43.5137 131.204 43.6188 130.889 43.6188Z" />
<path d="M151.244 79.5649C150.457 79.5649 149.775 78.9343 149.775 78.0934C149.775 77.3051 150.404 76.6219 151.244 76.6219L168.398 76.5694C169.185 76.5694 169.867 77.2 169.867 78.0409C169.867 78.8292 169.238 79.5123 168.398 79.5123L151.244 79.5649Z" />
<path d="M156.804 127.598C156.542 127.598 156.28 127.546 156.017 127.388L141.591 118.139C140.909 117.718 140.699 116.825 141.171 116.142C141.591 115.458 142.483 115.248 143.165 115.721L157.591 124.97C158.273 125.391 158.483 126.284 158.011 126.967C157.749 127.388 157.277 127.598 156.804 127.598Z" />
</g>
<defs>
<clipPath id="clip0_45_75">
<rect width="200" height="200" fill="currentColor" />
</clipPath>
</defs>
</svg>
);
}

View file

@ -1,6 +1,6 @@
import { useAuthenticatedData } from '@/app/common/auth';
import React, { useEffect } from 'react';
import useSWR from 'swr';
import { useAuthenticatedData } from "@/app/common/auth";
import React, { useEffect } from "react";
import useSWR from "swr";
import {
AlertDialog,
AlertDialogAction,
@ -12,8 +12,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import styles from './modelPicker.module.css';
import styles from "./modelPicker.module.css";
export interface Model {
id: number;
@ -23,10 +22,10 @@ export interface Model {
// Custom fetcher function to fetch options
const fetchOptionsRequest = async (url: string) => {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return response.json();
};
@ -35,21 +34,21 @@ export const useOptionsRequest = (url: string) => {
const { data, error } = useSWR<Model[]>(url, fetchOptionsRequest);
return {
data,
isLoading: !error && !data,
isError: error,
data,
isLoading: !error && !data,
isError: error,
};
};
const fetchSelectedModel = async (url: string) => {
const response = await fetch(url, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
});
return response.json();
}
};
export const useSelectedModel = (url: string) => {
const { data, error } = useSWR<Model>(url, fetchSelectedModel);
@ -58,8 +57,8 @@ export const useSelectedModel = (url: string) => {
data,
isLoading: !error && !data,
isError: error,
}
}
};
};
interface ModelPickerProps {
disabled?: boolean;
@ -68,17 +67,19 @@ interface ModelPickerProps {
}
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 { data: models } = useOptionsRequest("/api/model/chat/options");
const { data: selectedModel } = useSelectedModel("/api/model/chat");
const [openLoginDialog, setOpenLoginDialog] = React.useState(false);
let userData = useAuthenticatedData();
const setModelUsed = props.setModelUsed;
useEffect(() => {
if (props.setModelUsed && selectedModel) {
props.setModelUsed(selectedModel);
if (setModelUsed && selectedModel) {
setModelUsed(selectedModel);
}
}, [selectedModel]);
}, [selectedModel, setModelUsed]);
if (!models) {
return <div>Loading...</div>;
@ -94,14 +95,17 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
props.setModelUsed(model);
}
fetch('/api/config/data/conversation/model' + '?id=' + String(model.id), { method: 'POST', body: JSON.stringify(model) })
fetch("/api/model/chat" + "?id=" + String(model.id), {
method: "POST",
body: JSON.stringify(model),
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to select model');
throw new Error("Failed to select model");
}
})
.catch((error) => {
console.error('Failed to select model', error);
console.error("Failed to select model", error);
});
}
@ -114,15 +118,19 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
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}>
<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}
@ -132,18 +140,24 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
<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>
<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>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
window.location.href = window.location.origin + "/login";
}}
>
Sign in
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View file

@ -11,26 +11,15 @@ menu.menu a {
gap: 4px;
}
menu.menu a.selected {
background-color: var(--primary-hover);
}
menu.menu a:hover {
background-color: var(--primary-hover);
}
menu.menu {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
a.selected {
background-color: hsl(var(--accent));
}
div.titleBar {
display: grid;
grid-template-columns: 1fr auto;
padding: 16px 0;
margin: auto;
display: flex;
justify-content: space-between;
align-content: space-evenly;
align-items: start;
}
div.titleBar menu {
@ -58,11 +47,6 @@ div.settingsMenu {
align-items: center;
}
div.settingsMenu:hover {
background-color: var(--primary-hover);
cursor: pointer;
}
div.settingsMenuOptions {
display: block;
grid-auto-flow: row;
@ -75,14 +59,6 @@ div.settingsMenuOptions {
border-radius: 8px;
}
div.settingsMenuOptions a {
padding: 4px;
}
div.settingsMenuUsername {
font-weight: bold;
}
@media screen and (max-width: 600px) {
menu.menu span {
display: none;
@ -91,4 +67,8 @@ div.settingsMenuUsername {
div.settingsMenuOptions {
right: 4px;
}
div.titleBar {
padding: 8px;
}
}

View file

@ -1,106 +1,293 @@
'use client'
"use client";
import styles from './navMenu.module.css';
import Image from 'next/image';
import Link from 'next/link';
import { useAuthenticatedData, UserProfile } from '@/app/common/auth';
import { useState } from 'react';
import styles from "./navMenu.module.css";
import Link from "next/link";
import { useAuthenticatedData } from "@/app/common/auth";
import { useState, useEffect } from "react";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarTrigger,
} from "@/components/ui/menubar";
interface NavMenuProps {
selected: string;
}
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Moon,
Sun,
UserCircle,
User,
Robot,
MagnifyingGlass,
Question,
GearFine,
ArrowRight,
UsersFour,
} from "@phosphor-icons/react";
function SettingsMenu(props: UserProfile) {
const [showSettings, setShowSettings] = useState(false);
export default function NavMenu() {
const userData = useAuthenticatedData();
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [initialLoadDone, setInitialLoadDone] = useState(false);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 768);
});
if (localStorage.getItem("theme") === "dark") {
document.documentElement.classList.add("dark");
setDarkMode(true);
} else if (localStorage.getItem("theme") === "light") {
document.documentElement.classList.remove("dark");
setDarkMode(false);
} else {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
if (mq.matches) {
document.documentElement.classList.add("dark");
setDarkMode(true);
}
}
setInitialLoadDone(true);
}, []);
useEffect(() => {
if (!initialLoadDone) return;
toggleDarkMode(darkMode);
}, [darkMode, initialLoadDone]);
function toggleDarkMode(darkMode: boolean) {
if (darkMode) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
localStorage.setItem("theme", darkMode ? "dark" : "light");
}
return (
<div className={styles.settingsMenu}>
<div className={styles.settingsMenuProfile} onClick={() => setShowSettings(!showSettings)}>
<Image
src={props.photo || "/agents.svg"}
alt={props.username}
width={50}
height={50}
/>
</div>
{showSettings && (
<div className={styles.settingsMenuOptions}>
<div className={styles.settingsMenuUsername}>{props.username}</div>
<Link href="/config">
Settings
</Link>
<Link href="https://github.com/khoj-ai/khoj">
Github
</Link>
<Link href="https://docs.khoj.dev">
Help
</Link>
<Link href="/auth/logout">
Logout
</Link>
</div>
<div className={styles.titleBar}>
{isMobileWidth ? (
<DropdownMenu>
<DropdownMenuTrigger>
{userData ? (
<Avatar
className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}
>
<AvatarImage src={userData?.photo} alt="user profile" />
<AvatarFallback className="bg-transparent hover:bg-muted">
{userData?.username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
) : (
<UserCircle className="h-10 w-10" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent className="gap-2">
<DropdownMenuItem
onClick={() => setDarkMode(!darkMode)}
className="w-full cursor-pointer"
>
<div className="flex flex-rows">
{darkMode ? (
<Sun className="w-6 h-6" />
) : (
<Moon className="w-6 h-6" />
)}
<p className="ml-3 font-semibold">
{darkMode ? "Light Mode" : "Dark Mode"}
</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/agents" className="no-underline w-full">
<div className="flex flex-rows">
<UsersFour className="w-6 h-6" />
<p className="ml-3 font-semibold">Agents</p>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/automations" className="no-underline w-full">
<div className="flex flex-rows">
<Robot className="w-6 h-6" />
<p className="ml-3 font-semibold">Automations</p>
</div>
</Link>
</DropdownMenuItem>
{userData && (
<DropdownMenuItem>
<Link href="/search" className="no-underline w-full">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</DropdownMenuItem>
)}
<>
<DropdownMenuSeparator />
{userData && (
<DropdownMenuItem>
<Link href="/settings" className="no-underline w-full">
<div className="flex flex-rows">
<GearFine className="w-6 h-6" />
<p className="ml-3 font-semibold">Settings</p>
</div>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem>
<Link href="https://docs.khoj.dev" className="no-underline w-full">
<div className="flex flex-rows">
<Question className="w-6 h-6" />
<p className="ml-3 font-semibold">Help</p>
</div>
</Link>
</DropdownMenuItem>
{userData ? (
<DropdownMenuItem>
<Link href="/auth/logout" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Logout</p>
</div>
</Link>
</DropdownMenuItem>
) : (
<DropdownMenuItem>
<Link href="/auth/login" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Login</p>
</div>
</Link>
</DropdownMenuItem>
)}
</>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Menubar className="border-none">
<MenubarMenu>
<MenubarTrigger>
{userData ? (
<Avatar
className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}
>
<AvatarImage src={userData?.photo} alt="user profile" />
<AvatarFallback className="bg-transparent hover:bg-muted">
{userData?.username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
) : (
<UserCircle className="w-10 h-10" />
)}
</MenubarTrigger>
<MenubarContent align="end" className="rounded-xl gap-2">
<MenubarItem
onClick={() => setDarkMode(!darkMode)}
className="w-full hover:cursor-pointer"
>
<div className="flex flex-rows">
{darkMode ? (
<Sun className="w-6 h-6" />
) : (
<Moon className="w-6 h-6" />
)}
<p className="ml-3 font-semibold">
{darkMode ? "Light Mode" : "Dark Mode"}
</p>
</div>
</MenubarItem>
<MenubarItem>
<Link href="/agents" className="no-underline w-full">
<div className="flex flex-rows">
<UsersFour className="w-6 h-6" />
<p className="ml-3 font-semibold">Agents</p>
</div>
</Link>
</MenubarItem>
<MenubarItem>
<Link href="/automations" className="no-underline w-full">
<div className="flex flex-rows">
<Robot className="w-6 h-6" />
<p className="ml-3 font-semibold">Automations</p>
</div>
</Link>
</MenubarItem>
{userData && (
<MenubarItem>
<Link href="/search" className="no-underline w-full">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</MenubarItem>
)}
<>
<MenubarSeparator className="dark:bg-white height-[2px] bg-black" />
<MenubarItem>
<Link
href="https://docs.khoj.dev"
className="no-underline w-full"
>
<div className="flex flex-rows">
<Question className="w-6 h-6" />
<p className="ml-3 font-semibold">Help</p>
</div>
</Link>
</MenubarItem>
{userData && (
<MenubarItem>
<Link href="/settings" className="no-underline w-full">
<div className="flex flex-rows">
<GearFine className="w-6 h-6" />
<p className="ml-3 font-semibold">Settings</p>
</div>
</Link>
</MenubarItem>
)}
{userData ? (
<MenubarItem>
<Link href="/auth/logout" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Logout</p>
</div>
</Link>
</MenubarItem>
) : (
<MenubarItem>
<Link href="/auth/login" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Login</p>
</div>
</Link>
</MenubarItem>
)}
</>
</MenubarContent>
</MenubarMenu>
</Menubar>
)}
</div>
);
}
export default function NavMenu(props: NavMenuProps) {
let userData = useAuthenticatedData();
return (
<div className={styles.titleBar}>
<Link href="/">
<Image
src="/khoj-logo.svg"
alt="Khoj Logo"
className={styles.logo}
width={100}
height={50}
priority
/>
</Link>
<menu className={styles.menu}>
<a className={props.selected === "Chat" ? styles.selected : ""} href = '/chat'>
<Image
src="/chat.svg"
alt="Chat Logo"
className={styles.lgoo}
width={24}
height={24}
priority
/>
<span>
Chat
</span>
</a>
<a className={props.selected === "Agents" ? styles.selected : ""} href='/agents'>
<Image
src="/agents.svg"
alt="Agent Logo"
className={styles.lgoo}
width={24}
height={24}
priority
/>
<span>
Agents
</span>
</a>
<a className={props.selected === "Automations" ? styles.selected : ""} href = '/automations'>
<Image
src="/automation.svg"
alt="Automation Logo"
className={styles.lgoo}
width={24}
height={24}
priority
/>
<span>
Automations
</span>
</a>
{userData && <SettingsMenu {...userData} />}
</menu>
</div>
)
}

View file

@ -0,0 +1,55 @@
import React from "react";
import { ArrowRight } from "@phosphor-icons/react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
interface ProfileCardProps {
name: string;
avatar: JSX.Element;
link: string;
description?: string; // Optional description field
}
const ProfileCard: React.FC<ProfileCardProps> = ({ name, avatar, link, description }) => {
return (
<div className="relative group flex">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="flex items-center justify-center">
{avatar}
<div>{name}</div>
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="w-80 h-30">
{/* <div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"> */}
<a
href={link}
target="_blank"
rel="noreferrer"
className="mt-1 ml-2 block no-underline"
>
<div className="flex items-center justify-start gap-2">
{avatar}
<div className="mr-2 mt-1 flex justify-center items-center text-sm font-semibold text-gray-800">
{name}
<ArrowRight weight="bold" className="ml-1" />
</div>
</div>
</a>
{description && (
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
{description || "A Khoj agent"}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
};
export default ProfileCard;

View file

@ -1,31 +0,0 @@
div.panel {
padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
max-height: 80vh;
overflow-y: auto;
max-width: auto;
}
div.panel a {
color: var(--intense-green);
text-decoration: underline;
}
div.onlineReference,
div.contextReference {
margin: 4px;
border-radius: 8px;
padding: 4px;
}
div.contextReference:hover {
cursor: pointer;
}
div.singleReference {
padding: 8px;
border-radius: 8px;
background-color: var(--frosted-background-color);
margin-top: 8px;
}

View file

@ -1,193 +1,409 @@
'use client'
"use client";
import styles from "./referencePanel.module.css";
import { useEffect, useState } from "react";
import { useState } from "react";
import { ArrowRight } from "@phosphor-icons/react";
import markdownIt from "markdown-it";
const md = new markdownIt({
html: true,
linkify: true,
typographer: true
typographer: true,
});
import { SingleChatMessage, Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage";
import { Context, WebPage, OnlineContext } from "../chatMessage/chatMessage";
import { Card } from "@/components/ui/card";
interface ReferencePanelProps {
referencePanelData: SingleChatMessage | null;
setShowReferencePanel: (showReferencePanel: boolean) => void;
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import DOMPurify from "dompurify";
import { getIconFromFilename } from "@/app/common/iconUtils";
interface NotesContextReferenceData {
title: string;
content: string;
}
export function hasValidReferences(referencePanelData: SingleChatMessage | null) {
interface NotesContextReferenceCardProps extends NotesContextReferenceData {
showFullContent: boolean;
}
function extractSnippet(props: NotesContextReferenceCardProps): string {
const hierarchicalFileExtensions = ["org", "md", "markdown"];
const extension = props.title.split(".").pop() || "";
const cleanContent = hierarchicalFileExtensions.includes(extension)
? props.content.split("\n").slice(1).join("\n")
: props.content;
return props.showFullContent
? DOMPurify.sanitize(md.render(cleanContent))
: DOMPurify.sanitize(cleanContent);
}
function NotesContextReferenceCard(props: NotesContextReferenceCardProps) {
const fileIcon = getIconFromFilename(
props.title || ".txt",
"w-6 h-6 text-muted-foreground inline-flex mr-2",
);
const snippet = extractSnippet(props);
const [isHovering, setIsHovering] = useState(false);
return (
referencePanelData &&
(
(referencePanelData.context && referencePanelData.context.length > 0) ||
(referencePanelData.onlineContext && Object.keys(referencePanelData.onlineContext).length > 0 &&
Object.values(referencePanelData.onlineContext).some(
(onlineContextData) =>
(onlineContextData.webpages && onlineContextData.webpages.length > 0)|| onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph))
)
<>
<Popover open={isHovering && !props.showFullContent} onOpenChange={setIsHovering}>
<PopoverTrigger asChild>
<Card
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words text-balance rounded-lg p-2 bg-muted border-none`}
>
<h3
className={`${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground}`}
>
{fileIcon}
{props.title}
</h3>
<p
className={`${props.showFullContent ? "block" : "overflow-hidden line-clamp-2"}`}
dangerouslySetInnerHTML={{ __html: snippet }}
></p>
</Card>
</PopoverTrigger>
<PopoverContent className="w-[400px] mx-2">
<Card
className={`w-auto overflow-hidden break-words text-balance rounded-lg p-2 border-none`}
>
<h3 className={`line-clamp-2 text-muted-foreground}`}>
{fileIcon}
{props.title}
</h3>
<p
className={`overflow-hidden line-clamp-3`}
dangerouslySetInnerHTML={{ __html: snippet }}
></p>
</Card>
</PopoverContent>
</Popover>
</>
);
}
function CompiledReference(props: { context: (Context | string) }) {
export interface ReferencePanelData {
notesReferenceCardData: NotesContextReferenceData[];
onlineReferenceCardData: OnlineReferenceData[];
}
let snippet = "";
let file = "";
if (typeof props.context === "string") {
// Treat context as a string and get the first line for the file name
const lines = props.context.split("\n");
file = lines[0];
snippet = lines.slice(1).join("\n");
} else {
const context = props.context as Context;
snippet = context.compiled;
file = context.file;
interface OnlineReferenceData {
title: string;
description: string;
link: string;
}
interface OnlineReferenceCardProps extends OnlineReferenceData {
showFullContent: boolean;
}
function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
const [isHovering, setIsHovering] = useState(false);
if (!props.link || props.link.split(" ").length > 1) {
return null;
}
const [showSnippet, setShowSnippet] = useState(false);
let favicon = `https://www.google.com/s2/favicons?domain=globe`;
let domain = "unknown";
try {
domain = new URL(props.link).hostname;
favicon = `https://www.google.com/s2/favicons?domain=${domain}`;
} catch (error) {
console.warn(`Error parsing domain from link: ${props.link}`);
return null;
}
const handleMouseEnter = () => {
setIsHovering(true);
};
const handleMouseLeave = () => {
setIsHovering(false);
};
return (
<div className={styles.singleReference}>
<div className={styles.contextReference} onClick={() => setShowSnippet(!showSnippet)}>
<div className={styles.referencePanelTitle}>
{file}
</div>
<div className={styles.referencePanelContent} style={{ display: showSnippet ? "block" : "none" }}>
<div>
{snippet}
</div>
</div>
</div>
</div>
)
}
function WebPageReference(props: { webpages: WebPage, query: string | null }) {
let snippet = md.render(props.webpages.snippet);
const [showSnippet, setShowSnippet] = useState(false);
return (
<div className={styles.onlineReference} onClick={() => setShowSnippet(!showSnippet)}>
<div className={styles.onlineReferenceTitle}>
<a href={props.webpages.link} target="_blank" rel="noreferrer">
{
props.query ? (
<span>
{props.query}
</span>
) : <span>
{props.webpages.query}
</span>
}
</a>
</div>
<div className={styles.onlineReferenceContent} style={{ display: showSnippet ? "block" : "none" }}>
<div dangerouslySetInnerHTML={{ __html: snippet }}></div>
</div>
</div>
)
}
function OnlineReferences(props: { onlineContext: OnlineContextData, query: string}) {
const webpages = props.onlineContext.webpages;
const answerBox = props.onlineContext.answerBox;
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
const knowledgeGraph = props.onlineContext.knowledgeGraph;
return (
<div className={styles.singleReference}>
{
webpages && (
!Array.isArray(webpages) ? (
<WebPageReference webpages={webpages} query={props.query} />
) : (
webpages.map((webpage, index) => {
return <WebPageReference webpages={webpage} key={index} query={null} />
})
)
)
}
{
answerBox && (
<div className={styles.onlineReference}>
<div className={styles.onlineReferenceTitle}>
{answerBox.title}
</div>
<div className={styles.onlineReferenceContent}>
<div>
{answerBox.answer}
</div>
</div>
</div>
)
}
{
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
return (
<div className={styles.onlineReference} key={index}>
<div className={styles.onlineReferenceTitle}>
<a href={people.link} target="_blank" rel="noreferrer">
{people.question}
</a>
</div>
<div className={styles.onlineReferenceContent}>
<div>
{people.snippet}
<>
<Popover open={isHovering && !props.showFullContent} onOpenChange={setIsHovering}>
<PopoverTrigger asChild>
<Card
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words rounded-lg text-balance p-2 bg-muted border-none`}
>
<div className="flex flex-col">
<a
href={props.link}
target="_blank"
rel="noreferrer"
className="!no-underline p-2"
>
<div className="flex items-center">
<img src={favicon} alt="" className="!w-4 h-4 mr-2" />
<h3
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground`}
>
{domain}
</h3>
</div>
</div>
</div>
)
})
}
{
knowledgeGraph && (
<div className={styles.onlineReference}>
<div className={styles.onlineReferenceTitle}>
<a href={knowledgeGraph.descriptionLink} target="_blank" rel="noreferrer">
{knowledgeGraph.title}
<h3
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} font-bold`}
>
{props.title}
</h3>
<p
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"}`}
>
{props.description}
</p>
</a>
</div>
<div className={styles.onlineReferenceContent}>
<div>
{knowledgeGraph.description}
</div>
</Card>
</PopoverTrigger>
<PopoverContent className="w-[400px] mx-2">
<Card
className={`w-auto overflow-hidden break-words text-balance rounded-lg border-none`}
>
<div className="flex flex-col">
<a
href={props.link}
target="_blank"
rel="noreferrer"
className="!no-underline p-2"
>
<div className="flex items-center">
<img src={favicon} alt="" className="!w-4 h-4 mr-2" />
<h3
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"} text-muted-foreground`}
>
{domain}
</h3>
</div>
<h3
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"} font-bold`}
>
{props.title}
</h3>
<p
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-3"}`}
>
{props.description}
</p>
</a>
</div>
</div>
)
</Card>
</PopoverContent>
</Popover>
</>
);
}
export function constructAllReferences(contextData: Context[], onlineData: OnlineContext) {
const onlineReferences: OnlineReferenceData[] = [];
const contextReferences: NotesContextReferenceData[] = [];
if (onlineData) {
let localOnlineReferences = [];
for (const [key, value] of Object.entries(onlineData)) {
if (value.answerBox) {
localOnlineReferences.push({
title: value.answerBox.title,
description: value.answerBox.answer,
link: value.answerBox.source,
});
}
if (value.knowledgeGraph) {
localOnlineReferences.push({
title: value.knowledgeGraph.title,
description: value.knowledgeGraph.description,
link: value.knowledgeGraph.descriptionLink,
});
}
if (value.webpages) {
// If webpages is of type Array, iterate through it and add each webpage to the localOnlineReferences array
if (value.webpages instanceof Array) {
let webPageResults = value.webpages.map((webPage) => {
return {
title: webPage.query,
description: webPage.snippet,
link: webPage.link,
};
});
localOnlineReferences.push(...webPageResults);
} else {
let singleWebpage = value.webpages as WebPage;
// If webpages is an object, add the object to the localOnlineReferences array
localOnlineReferences.push({
title: singleWebpage.query,
description: singleWebpage.snippet,
link: singleWebpage.link,
});
}
}
if (value.organic) {
let organicResults = value.organic.map((organicContext) => {
return {
title: organicContext.title,
description: organicContext.snippet,
link: organicContext.link,
};
});
localOnlineReferences.push(...organicResults);
}
}
onlineReferences.push(...localOnlineReferences);
}
if (contextData) {
let localContextReferences = contextData.map((context) => {
if (!context.compiled) {
const fileContent = context as unknown as string;
const title = fileContent.split("\n")[0];
const content = fileContent.split("\n").slice(1).join("\n");
return {
title: title,
content: content,
};
}
return {
title: context.file,
content: context.compiled,
};
});
contextReferences.push(...localContextReferences);
}
return {
notesReferenceCardData: contextReferences,
onlineReferenceCardData: onlineReferences,
};
}
export interface TeaserReferenceSectionProps {
notesReferenceCardData: NotesContextReferenceData[];
onlineReferenceCardData: OnlineReferenceData[];
isMobileWidth: boolean;
}
export function TeaserReferencesSection(props: TeaserReferenceSectionProps) {
const [numTeaserSlots, setNumTeaserSlots] = useState(3);
useEffect(() => {
setNumTeaserSlots(props.isMobileWidth ? 1 : 3);
}, [props.isMobileWidth]);
const notesDataToShow = props.notesReferenceCardData.slice(0, numTeaserSlots);
const onlineDataToShow =
notesDataToShow.length < numTeaserSlots
? props.onlineReferenceCardData.slice(0, numTeaserSlots - notesDataToShow.length)
: [];
const shouldShowShowMoreButton =
props.notesReferenceCardData.length > 0 || props.onlineReferenceCardData.length > 0;
const numReferences =
props.notesReferenceCardData.length + props.onlineReferenceCardData.length;
if (numReferences === 0) {
return null;
}
return (
<div className="pt-0 px-4 pb-4 md:px-6">
<h3 className="inline-flex items-center">
References
<p className="text-gray-400 m-2">{numReferences} sources</p>
</h3>
<div className={`flex flex-wrap gap-2 w-auto mt-2`}>
{notesDataToShow.map((note, index) => {
return (
<NotesContextReferenceCard
showFullContent={false}
{...note}
key={`${note.title}-${index}`}
/>
);
})}
{onlineDataToShow.map((online, index) => {
return (
<GenericOnlineReferenceCard
showFullContent={false}
{...online}
key={`${online.title}-${index}`}
/>
);
})}
{shouldShowShowMoreButton && (
<ReferencePanel
notesReferenceCardData={props.notesReferenceCardData}
onlineReferenceCardData={props.onlineReferenceCardData}
/>
)}
</div>
</div>
)
);
}
export default function ReferencePanel(props: ReferencePanelProps) {
interface ReferencePanelDataProps {
notesReferenceCardData: NotesContextReferenceData[];
onlineReferenceCardData: OnlineReferenceData[];
}
if (!props.referencePanelData) {
export default function ReferencePanel(props: ReferencePanelDataProps) {
if (!props.notesReferenceCardData && !props.onlineReferenceCardData) {
return null;
}
if (!hasValidReferences(props.referencePanelData)) {
return null;
}
return (
<div className={`${styles.panel}`}>
References <button onClick={() => props.setShowReferencePanel(false)}>Hide</button>
{
props.referencePanelData?.context.map((context, index) => {
return <CompiledReference context={context} key={index} />
})
}
{
Object.entries(props.referencePanelData?.onlineContext || {}).map(([key, onlineContextData], index) => {
return <OnlineReferences onlineContext={onlineContextData} query={key} key={index} />
})
}
</div>
);
return (
<Sheet>
<SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center !m-2 inline-flex">
View references
<ArrowRight className="m-1" />
</SheetTrigger>
<SheetContent className="overflow-y-scroll">
<SheetHeader>
<SheetTitle>References</SheetTitle>
<SheetDescription>View all references for this response</SheetDescription>
</SheetHeader>
<div className="flex flex-wrap gap-2 w-auto mt-2">
{props.notesReferenceCardData.map((note, index) => {
return (
<NotesContextReferenceCard
showFullContent={true}
{...note}
key={`${note.title}-${index}`}
/>
);
})}
{props.onlineReferenceCardData.map((online, index) => {
return (
<GenericOnlineReferenceCard
showFullContent={true}
{...online}
key={`${online.title}-${index}`}
/>
);
})}
</div>
</SheetContent>
</Sheet>
);
}

View file

@ -7,9 +7,10 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Share } from "@phosphor-icons/react";
interface ShareLinkProps {
buttonTitle: string;
@ -17,6 +18,9 @@ interface ShareLinkProps {
description: string;
url: string;
onShare: () => void;
buttonVariant?: keyof typeof buttonVariants;
includeIcon?: boolean;
buttonClassName?: string;
}
function copyToClipboard(text: string) {
@ -30,33 +34,37 @@ function copyToClipboard(text: string) {
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}>
<DialogTrigger asChild onClick={props.onShare}>
<Button
size="sm"
className={`${props.buttonClassName || "px-3"}`}
variant={props.buttonVariant ?? ("default" as const)}
>
{props.includeIcon && <Share className="w-4 h-4 mr-2" />}
{props.buttonTitle}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.title}</DialogTitle>
<DialogDescription>
{props.description}
</DialogDescription>
<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 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>
<Button type="submit" size="sm" className="px-3" onClick={() => copyToClipboard(props.url)}>
<span>Copy</span>
</Button>
</div>
</DialogContent>
</Dialog>
);

View file

@ -2,9 +2,29 @@ div.session {
padding: 0.5rem;
margin-bottom: 0.25rem;
border-radius: 0.5rem;
color: var(--main-text-color);
cursor: pointer;
max-width: 14rem;
font-size: medium;
display: grid;
grid-template-columns: minmax(auto, 400px) 1fr;
gap: 0rem;
}
div.compressed {
grid-template-columns: minmax(12rem, 100%) 1fr 1fr;
}
div.sessionHover {
background-color: hsla(var(--popover));
}
div.session:hover {
background-color: hsla(var(--popover));
color: hsla(var(--popover-foreground));
}
div.session a {
text-decoration: none;
}
button.button {
@ -17,44 +37,41 @@ button.button {
}
button.showMoreButton {
background: var(--intense-green);
border: none;
color: var(--frosted-background-color);
border-radius: 0.5rem;
padding: 8px;
}
div.panel {
display: grid;
grid-auto-flow: row;
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 1rem;
background-color: var(--calm-blue);
color: var(--main-text-color);
height: 100%;
overflow-y: auto;
max-width: auto;
transition: background-color 0.5s;
}
div.expanded {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
background-color: hsla(var(--muted));
height: 100%;
}
div.collapsed {
display: grid;
grid-template-columns: 1fr;
}
div.session:hover {
background-color: var(--calmer-blue);
height: fit-content;
padding-top: 1rem;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
}
p.session {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-align: left;
font-size: small;
}
div.header {
@ -62,12 +79,19 @@ div.header {
grid-template-columns: 1fr auto;
}
img.profile {
width: 24px;
height: 24px;
border-radius: 50%;
div.profile {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
margin-top: auto;
}
div.panelWrapper {
display: grid;
grid-template-rows: 1fr auto auto;
height: 100%;
}
div.modalSessionsList {
position: fixed;
@ -75,7 +99,7 @@ div.modalSessionsList {
left: 0;
width: 100%;
height: 100%;
background-color: var(--frosted-background-color);
background-color: hsla(var(--frosted-background-color));
z-index: 1;
display: flex;
justify-content: center;
@ -86,7 +110,7 @@ div.modalSessionsList {
div.modalSessionsList div.content {
max-width: 80%;
max-height: 80%;
background-color: var(--frosted-background-color);
background-color: hsla(var(--frosted-background-color));
overflow: auto;
padding: 20px;
border-radius: 10px;
@ -96,3 +120,36 @@ div.modalSessionsList div.session {
max-width: 100%;
text-overflow: ellipsis;
}
@media screen and (max-width: 768px) {
div.panel {
padding: 0.25rem;
/* position: fixed; */
width: 100%;
}
div.expanded {
z-index: 1;
}
div.singleReference {
padding: 4px;
}
div.panelWrapper {
width: 100%;
padding: 0 1rem;
}
div.session.compressed {
max-width: 100%;
display: grid;
grid-template-columns: 70vw auto;
justify-content: space-between;
}
div.session {
max-width: 100%;
grid-template-columns: minmax(auto, 70vw) 1fr;
}
}

View file

@ -1,32 +1,64 @@
'use client'
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import styles from "./suggestions.module.css";
import { getIconFromIconName } from "@/app/common/iconUtils";
import { converColorToBgGradient } from "@/app/common/colorUtils";
function convertSuggestionTitleToIconClass(title: string, color: string) {
if (title.includes("automation")) return getIconFromIconName("Robot", color, "w-8", "h-8");
if (title.includes("online")) return getIconFromIconName("Globe", color, "w-8", "h-8");
if (title.includes("paint")) return getIconFromIconName("Palette", color, "w-8", "h-8");
if (title.includes("pop")) return getIconFromIconName("Confetti", color, "w-8", "h-8");
if (title.includes("travel")) return getIconFromIconName("Jeep", color, "w-8", "h-8");
if (title.includes("learn")) return getIconFromIconName("Book", color, "w-8", "h-8");
if (title.includes("health")) return getIconFromIconName("Asclepius", color, "w-8", "h-8");
if (title.includes("fun")) return getIconFromIconName("Island", color, "w-8", "h-8");
if (title.includes("home")) return getIconFromIconName("House", color, "w-8", "h-8");
if (title.includes("language")) return getIconFromIconName("Translate", color, "w-8", "h-8");
if (title.includes("code")) return getIconFromIconName("Code", color, "w-8", "h-8");
else return getIconFromIconName("Lightbulb", color, "w-8", "h-8");
}
interface SuggestionCardProps {
title: string;
body: string;
link: string;
styleClass: string;
title: string;
body: string;
link: string;
color: string;
}
export default function SuggestionCard(data: SuggestionCardProps) {
const bgColors = converColorToBgGradient(data.color);
const cardClassName = `${styles.card} ${bgColors} md:w-full md:h-fit sm:w-full sm:h-fit lg:w-[200px] lg:h-[200px] cursor-pointer`;
const titleClassName = `${styles.title} pt-2 dark:text-white dark:font-bold`;
const descriptionClassName = `${styles.text} dark:text-white`;
return (
<div className={styles[data.styleClass] + " " + styles.card}>
<div className={styles.title}>
{data.title}
</div>
<div className={styles.body}>
{data.body}
</div>
<div>
<a
href={data.link}
target="_blank"
rel="noopener noreferrer"
>click me
</a>
</div>
</div>
);
const cardContent = (
<Card className={cardClassName}>
<CardHeader className="m-0 p-2 pb-1 relative">
<div className="flex flex-row md:flex-col">
{convertSuggestionTitleToIconClass(
data.title.toLowerCase(),
data.color.toLowerCase(),
)}
<CardTitle className={titleClassName}>{data.title}</CardTitle>
</div>
</CardHeader>
<CardContent className="m-0 p-2 pr-4 pt-1">
<CardDescription
className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4`}
>
{data.body}
</CardDescription>
</CardContent>
</Card>
);
return data.link ? (
<a href={data.link} className="no-underline">
{cardContent}
</a>
) : (
cardContent
);
}

View file

@ -1,40 +1,18 @@
div.pink {
background-color: #f8d1f8;
color: #000000;
}
div.blue {
background-color: #d1f8f8;
color: #000000;
}
div.green {
background-color: #d1f8d1;
color: #000000;
}
div.purple {
background-color: #f8d1f8;
color: #000000;
}
div.yellow {
background-color: #f8f8d1;
color: #000000;
}
div.card {
padding: 1rem;
margin: 1rem;
border: 1px solid #000000;
.card {
padding: 0.5rem;
margin: 0.05rem;
border-radius: 0.5rem;
}
div.title {
font-size: 1.5rem;
font-weight: bold;
.title {
font-size: 1.0rem;
}
div.body {
font-size: 1rem;
.text {
padding-top: 0.2rem;
font-size: 0.9rem;
white-space: wrap;
padding-right: 4px;
color: black;
}

View file

@ -0,0 +1,405 @@
export interface Suggestion {
type: string;
color: string;
description: string;
link: string;
}
//samples for suggestion cards (should be moved to json later)
export const suggestionsData: Suggestion[] = [
{
type: "Automation",
color: "blue",
description: "Send me a summary of HackerNews every morning.",
link: "/automations?subject=Summarizing%20Top%20Headlines%20from%20HackerNews&query=Summarize%20the%20top%20headlines%20on%20HackerNews&crontime=00%207%20*%20*%20*",
},
{
type: "Automation",
color: "blue",
description: "Compose a bedtime story that a five-year-old might enjoy.",
link: "/automations?subject=Daily%20Bedtime%20Story&query=Compose%20a%20bedtime%20story%20that%20a%20five-year-old%20might%20enjoy.%20It%20should%20not%20exceed%20five%20paragraphs.%20Appeal%20to%20the%20imagination%2C%20but%20weave%20in%20learnings.&crontime=0%2021%20*%20*%20*",
},
{
type: "Paint",
color: "green",
description: "Paint a picture of a sunset but it's made of stained glass tiles",
link: "",
},
{
type: "Travel",
color: "yellow",
description: "Search for the best attractions in Austria Hungary",
link: "",
},
{
type: "Health",
color: "orange",
description: "Generate a weekly meal plan with recipes.",
link: "/automations?subject=Weekly Meal Plan&query=Create a weekly meal plan with 7 dinner recipes, including ingredients and brief instructions. Focus on balanced, healthy meals.&crontime=0 18 * * 0",
},
{
type: "Paint",
color: "green",
description: "Paint a futuristic cityscape with flying cars.",
link: "",
},
{
type: "Travel",
color: "yellow",
description: "Find the top-rated coffee shops in Seattle.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Send daily motivational quotes.",
link: "/automations?subject=Daily Motivation&query=Provide an inspiring quote for the day along with a brief explanation of its meaning.&crontime=0 7 * * *",
},
{
type: "Paint",
color: "green",
description: "Create an abstract representation of jazz music.",
link: "",
},
{
type: "Learning",
color: "yellow",
description: "Research the history of the Eiffel Tower.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Compile a weekly news summary.",
link: "/automations?subject=Weekly News Digest&query=Summarize the top 5 most important news stories of the week across various categories.&crontime=0 18 * * 5",
},
{
type: "Paint",
color: "green",
description: "Paint a portrait of a cat wearing a Victorian-era costume.",
link: "",
},
{
type: "Travel",
color: "yellow",
description: "Find beginner-friendly hiking trails near Los Angeles.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Generate a daily writing prompt.",
link: "/automations?subject=Daily Writing Prompt&query=Create an engaging writing prompt suitable for short story or journal writing.&crontime=0 9 * * *",
},
{
type: "Paint",
color: "green",
description: "Create a surrealist landscape inspired by Salvador Dali.",
link: "",
},
{
type: "Learning",
color: "yellow",
description: "Research the benefits and drawbacks of electric vehicles.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Send weekly language learning tips for Spanish.",
link: "/automations?subject=Spanish Learning Tips&query=Provide a useful Spanish language learning tip, including vocabulary, grammar, or cultural insight.&crontime=0 19 * * 2",
},
{
type: "Paint",
color: "green",
description: "Paint a scene from a fairy tale in the style of Studio Ghibli.",
link: "",
},
{
type: "Pop Culture",
color: "yellow",
description: "Find the best-rated science fiction books of the last decade.",
link: "",
},
{
type: "Paint",
color: "green",
description: "Paint a still life of exotic fruits in a neon color palette.",
link: "",
},
{
type: "Travel",
color: "yellow",
description: "Research the most eco-friendly cities in Europe.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Send daily reminders for habit tracking.",
link: "/automations?subject=Habit Tracker&query=Generate a daily reminder to track habits, including a motivational message.&crontime=0 20 * * *",
},
{
type: "Paint",
color: "green",
description: "Create a digital painting of a cyberpunk street market.",
link: "",
},
{
type: "Learning",
color: "yellow",
description:
"Summarize the biography of this figure: https://en.wikipedia.org/wiki/Jean_Baptiste_Point_du_Sable",
link: "",
},
{
type: "Language",
color: "blue",
description: "Send daily Spanish phrases used in Latin America.",
link: "/automations?subject=Daily Latin American Spanish&query=Provide a common Spanish phrase or slang term used in Latin America, its meaning, and an example of usage. Include which countries it's most common in.&crontime=0 8 * * *",
},
{
type: "Paint",
color: "green",
description: "Create a vibrant painting inspired by Frida Kahlo's style.",
link: "",
},
{
type: "Food",
color: "yellow",
description: "Find the best empanada recipe from Colombia countries.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Weekly update on the Brazilian startup ecosystems.",
link: "/automations?subject=LatAm Startup News&query=Provide a summary of the most significant developments in Latin American startup ecosystems this week. Include notable funding rounds, expansions, or policy changes.&crontime=0 18 * * 5",
},
{
type: "Paint",
color: "green",
description:
"Paint a colorful scene of a traditional Day of the Dead celebration in Mexico.",
link: "",
},
{
type: "Language",
color: "blue",
description: "Daily Swahili phrase with English translation.",
link: "/automations?subject=Daily Swahili Lesson&query=Provide a common Swahili phrase or proverb, its English translation, and a brief explanation of its cultural significance in East Africa.&crontime=0 7 * * *",
},
{
type: "Paint",
color: "green",
description: "Create a digital painting of the Serengeti during wildebeest migration.",
link: "",
},
{
type: "Learning",
color: "yellow",
description: "Research the top M-Pesa alternatives in East Africa.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Weekly update on East African tech startups and innovations.",
link: "/automations?subject=East African Tech News&query=Summarize the most significant developments in East African tech startups and innovations this week. Include notable funding rounds, new product launches, or policy changes affecting the tech ecosystem.&crontime=0 18 * * 5",
},
{
type: "Paint",
color: "green",
description: "Paint a colorful scene inspired by Maasai traditional clothing and jewelry.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Weekly summary of EU policy changes and their impact.",
link: "/automations?subject=EU Policy Update&query=Summarize the most significant EU policy changes or proposals from this week. Explain their potential impact on European citizens and businesses.&crontime=0 17 * * 5",
},
{
type: "Paint",
color: "green",
description: "Paint a digital landscape of the Northern Lights over the Norwegian fjords.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Daily East Asian proverb with explanation.",
link: "/automations?subject=East Asian Wisdom&query=Provide a proverb from an East Asian language (rotating through Chinese, Japanese, Korean, etc.), its English translation, and a brief explanation of its cultural significance and practical application.&crontime=0 7 * * *",
},
{
type: "Paint",
color: "green",
description:
"Create a digital painting in the style of traditional Chinese ink wash landscape.",
link: "",
},
{
type: "Pop Culture",
color: "yellow",
description: "Research the latest trends in K-pop and its global influence.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Weekly summary of technological innovations from East Asian tech giants.",
link: "/automations?subject=East Asian Tech Update&query=Summarize the most significant technological innovations or product launches from major East Asian tech companies (e.g., Samsung, Sony, Alibaba, Tencent) this week. Explain their potential impact on global markets.&crontime=0 18 * * 5",
},
{
type: "Paint",
color: "green",
description: "Paint a vibrant scene of a Japanese cherry blossom festival.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Daily South Asian recipe with cultural significance.",
link: "/automations?subject=South Asian Culinary Journey&query=Provide a traditional South Asian recipe (rotating through Indian, Pakistani, Bangladeshi, Sri Lankan, etc. cuisines), including ingredients, brief instructions, and its cultural significance or origin story.&crontime=0 10 * * *",
},
{
type: "Pop Culture",
color: "yellow",
description: "Research the impact of Bollywood on global cinema and fashion.",
link: "",
},
{
type: "Automation",
color: "blue",
description: "Weekly update on South Asian startup ecosystems and innovations.",
link: "/automations?subject=South Asian Startup Pulse&query=Summarize the most significant developments in South Asian startup ecosystems this week. Include notable funding rounds, innovative solutions to local challenges, and any policy changes affecting the tech landscape in countries like India, Bangladesh, Pakistan, and Sri Lanka.&crontime=0 18 * * 5",
},
{
type: "Interviewing",
color: "purple",
description: "Create interview prep questions for a consulting job.",
link: "",
},
{
type: "Home",
color: "purple",
description: "Recommend plants that can grow well indoors.",
link: "",
},
{
type: "Health",
color: "purple",
description: "Suggest healthy meal prep ideas for a busy work week.",
link: "",
},
{
type: "Learning",
color: "purple",
description: "List effective time management techniques for students.",
link: "",
},
{
type: "Learning",
color: "purple",
description: "Provide tips for improving public speaking skills.",
link: "",
},
{
type: "Learning",
color: "purple",
description: "Recommend books for learning about personal finance.",
link: "",
},
{
type: "Home",
color: "purple",
description: "Suggest ways to reduce plastic waste in daily life.",
link: "",
},
{
type: "Health",
color: "purple",
description: "Create a beginner's guide to meditation and mindfulness.",
link: "",
},
{
type: "Fun",
color: "purple",
description: "List creative date ideas for couples on a budget.",
link: "",
},
{
type: "Interviewing",
color: "purple",
description: "Provide tips for writing an effective resume.",
link: "",
},
{
type: "Code",
color: "purple",
description: "Explain the concept of recursion with a simple coding example.",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Provide a coding challenge to reverse a string without using built-in functions.",
link: "",
},
{
type: "Code",
color: "purple",
description: "Explain the difference between 'let', 'const', and 'var' in JavaScript.",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Create a coding exercise to implement a basic sorting algorithm (e.g., bubble sort).",
link: "",
},
{
type: "Code",
color: "purple",
description: "Explain object-oriented programming principles with a simple class example.",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Provide a coding challenge to find the longest palindromic substring in a given string.",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Explain the concept of asynchronous programming with a JavaScript Promise example.",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Create a coding exercise to implement a basic data structure (e.g., linked list or stack).",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Explain the time and space complexity of common algorithms (e.g., binary search).",
link: "",
},
{
type: "Code",
color: "purple",
description:
"Provide a coding challenge to implement a simple REST API using Node.js and Express.",
link: "",
},
];

View file

@ -13,6 +13,11 @@ input.factVerification {
font-size: large;
}
div.factCheckerContainer {
width: 75vw;
margin: auto;
}
input.factVerification:focus {
outline: none;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
@ -123,6 +128,12 @@ div.dot2 {
animation-delay: -1.0s;
}
@media screen and (max-width: 768px) {
div.factCheckerContainer {
width: 95vw;
}
}
@-webkit-keyframes sk-rotate {
100% {
-webkit-transform: rotate(360deg)

View file

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

View file

@ -1,13 +1,11 @@
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.",
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',
icon: "/static/favicon.ico",
},
};
@ -16,10 +14,5 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className={styles.factCheckerLayout}>
<NavMenu selected="none" />
{children}
</div>
);
return <div>{children}</div>;
}

View file

@ -1,30 +1,28 @@
'use client'
"use client";
import styles from './factChecker.module.css';
import { useAuthenticatedData } from '@/app/common/auth';
import { useState, useEffect } from 'react';
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';
import ChatMessage, {
Context,
OnlineContext,
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";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
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 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}>
@ -44,12 +42,9 @@ interface SupplementReferences {
linkTitle: string;
}
interface ResponseWithReferences {
context?: Context[];
online?: {
[key: string]: OnlineContextData
}
online?: OnlineContext;
response?: string;
}
@ -71,53 +66,51 @@ function handleCompiledReferences(chunk: string, currentResponse: string) {
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");
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 = "";
const reader = response.body?.getReader();
let decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
while (true) {
const { done, value } = await reader.read();
if (done) break;
let chunk = decoder.decode(value, { stream: true });
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);
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);
}
} 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" });
@ -126,13 +119,16 @@ async function spawnNewConversation(setConversationID: (conversationID: string)
setConversationID(data.conversation_id);
}
interface ReferenceVerificationProps {
message: string;
additionalLink: string;
conversationId: string;
linkTitle: string;
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
setChildReferencesCallback: (
additionalLink: string,
response: string,
linkTitle: string,
) => void;
prefilledResponse?: string;
}
@ -140,15 +136,27 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
const [initialResponse, setInitialResponse] = useState("");
const [isLoading, setIsLoading] = useState(true);
const verificationStatement = `${props.message}. Use this link for reference: ${props.additionalLink}`;
const [isMobileWidth, setIsMobileWidth] = useState(false);
useEffect(() => {
if (props.prefilledResponse) {
setInitialResponse(props.prefilledResponse);
setIsLoading(false);
} else {
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => {});
verifyStatement(
verificationStatement,
props.conversationId,
setIsLoading,
setInitialResponse,
() => {},
);
}
setIsMobileWidth(window.innerWidth < 768);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 768);
});
}, [verificationStatement, props.conversationId, props.prefilledResponse]);
useEffect(() => {
@ -157,28 +165,30 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
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);
props.setChildReferencesCallback(
props.additionalLink,
initialResponse,
props.linkTitle,
);
}
}, [initialResponse, isLoading, props]);
return (
<div>
{isLoading &&
<LoadingSpinner />
}
<ChatMessage chatMessage={
{
{isLoading && <LoadingSpinner />}
<ChatMessage
chatMessage={{
automationId: "",
by: "AI",
intent: {},
message: initialResponse,
context: [],
created: (new Date()).toISOString(),
onlineContext: {}
}
} setReferencePanelData={() => {}} setShowReferencePanel={() => {}} />
created: new Date().toISOString(),
onlineContext: {},
}}
isMobileWidth={isMobileWidth}
/>
</div>
)
);
}
interface SupplementalReferenceProps {
@ -186,7 +196,11 @@ interface SupplementalReferenceProps {
officialFactToVerify: string;
conversationId: string;
additionalLink: string;
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
setChildReferencesCallback: (
additionalLink: string,
response: string,
linkTitle: string,
) => void;
prefilledResponse?: string;
linkTitle?: string;
}
@ -197,7 +211,12 @@ function SupplementalReference(props: SupplementalReferenceProps) {
return (
<Card className={`mt-2 mb-4`}>
<CardHeader>
<a className={styles.titleLink} href={props.additionalLink} target="_blank" rel="noreferrer">
<a
className={styles.titleLink}
href={props.additionalLink}
target="_blank"
rel="noreferrer"
>
{linkTitle}
</a>
<WebPageLink {...linkAsWebpage} />
@ -209,7 +228,8 @@ function SupplementalReference(props: SupplementalReferenceProps) {
linkTitle={linkTitle}
conversationId={props.conversationId}
setChildReferencesCallback={props.setChildReferencesCallback}
prefilledResponse={props.prefilledResponse} />
prefilledResponse={props.prefilledResponse}
/>
</CardContent>
</Card>
);
@ -219,13 +239,17 @@ 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">
<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("");
@ -236,6 +260,7 @@ export default function FactChecker() {
const [initialReferences, setInitialReferences] = useState<ResponseWithReferences>();
const [childReferences, setChildReferences] = useState<SupplementReferences[]>();
const [modelUsed, setModelUsed] = useState<Model>();
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [conversationID, setConversationID] = useState("");
const [runId, setRunId] = useState("");
@ -243,14 +268,28 @@ export default function FactChecker() {
const [initialModel, setInitialModel] = useState<Model>();
function setChildReferencesCallback(additionalLink: string, response: string, linkTitle: string) {
function setChildReferencesCallback(
additionalLink: string,
response: string,
linkTitle: string,
) {
const newReferences = childReferences || [];
const exists = newReferences.find((reference) => reference.additionalLink === additionalLink);
const exists = newReferences.find(
(reference) => reference.additionalLink === additionalLink,
);
if (exists) return;
newReferences.push({ additionalLink, response, linkTitle });
setChildReferences(newReferences);
}
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 768);
});
}, []);
let userData = useAuthenticatedData();
function storeData() {
@ -264,13 +303,13 @@ export default function FactChecker() {
};
fetch(`/api/chat/store/factchecker`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
"runId": runId,
"storeData": data
runId: runId,
storeData: data,
}),
});
}
@ -279,25 +318,25 @@ export default function FactChecker() {
if (factToVerify) {
document.title = `AI Fact Check: ${factToVerify}`;
} else {
document.title = 'AI Fact Checker';
document.title = "AI Fact Checker";
}
}, [factToVerify]);
useEffect(() => {
const storedFact = localStorage.getItem('factToVerify');
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');
const factToVerifyParam = urlParams.get("factToVerify");
if (factToVerifyParam) {
setFactToVerify(factToVerifyParam);
}
const runIdParam = urlParams.get('runId');
const runIdParam = urlParams.get("runId");
if (runIdParam) {
setRunId(runIdParam);
@ -326,7 +365,6 @@ export default function FactChecker() {
// Call the async function
fetchData();
}
}, []);
function onClickVerify() {
@ -350,9 +388,13 @@ export default function FactChecker() {
spawnNewConversation(setConversationID);
// Set the runId to a random 12-digit alphanumeric string
const newRunId = [...Array(16)].map(() => Math.random().toString(36)[2]).join('');
const newRunId = [...Array(16)].map(() => Math.random().toString(36)[2]).join("");
setRunId(newRunId);
window.history.pushState({}, document.title, window.location.pathname + `?runId=${newRunId}`);
window.history.pushState(
{},
document.title,
window.location.pathname + `?runId=${newRunId}`,
);
setOfficialFactToVerify(factToVerify);
setClickedVerify(false);
@ -360,30 +402,42 @@ export default function FactChecker() {
useEffect(() => {
if (!conversationID) return;
verifyStatement(officialFactToVerify, conversationID, setIsLoading, setInitialResponse, setInitialReferences);
verifyStatement(
officialFactToVerify,
conversationID,
setIsLoading,
setInitialResponse,
setInitialReferences,
);
}, [conversationID, officialFactToVerify]);
// Store factToVerify in localStorage whenever it changes
useEffect(() => {
localStorage.setItem('factToVerify', factToVerify);
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);
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 = document.createElement("meta");
metaOgTag.setAttribute("property", "og:description");
document.getElementsByTagName("head")[0].appendChild(metaOgTag);
}
metaOgTag.setAttribute('content', initialResponse);
metaOgTag.setAttribute("content", initialResponse);
}, [initialResponse]);
const renderReferences = (conversationId: string, initialReferences: ResponseWithReferences, officialFactToVerify: string, loadedFromStorage: boolean, childReferences?: SupplementReferences[]) => {
const renderReferences = (
conversationId: string,
initialReferences: ResponseWithReferences,
officialFactToVerify: string,
loadedFromStorage: boolean,
childReferences?: SupplementReferences[],
) => {
if (loadedFromStorage && childReferences) {
return renderSupplementalReferences(childReferences);
}
@ -397,7 +451,7 @@ export default function FactChecker() {
if (webpages instanceof Array) {
for (let i = 0; i < webpages.length; i++) {
const webpage = webpages[i];
const additionalLink = webpage.link || '';
const additionalLink = webpage.link || "";
if (seenLinks.has(additionalLink)) {
return null;
}
@ -405,7 +459,7 @@ export default function FactChecker() {
}
} else {
let singleWebpage = webpages as WebPage;
const additionalLink = singleWebpage.link || '';
const additionalLink = singleWebpage.link || "";
if (seenLinks.has(additionalLink)) {
return null;
}
@ -413,33 +467,36 @@ export default function FactChecker() {
}
});
return Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
let 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 || '';
// 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;
if (!seenLinks.has(additionalLink)) {
break;
}
}
}
seenLinks.add(additionalLink);
seenLinks.add(additionalLink);
if (additionalLink === '') return null;
if (additionalLink === "") return null;
return (
<SupplementalReference
key={index}
onlineData={onlineData}
officialFactToVerify={officialFactToVerify}
conversationId={conversationId}
additionalLink={additionalLink}
setChildReferencesCallback={setChildReferencesCallback} />
);
}).filter(Boolean);
return (
<SupplementalReference
key={index}
onlineData={onlineData}
officialFactToVerify={officialFactToVerify}
conversationId={conversationId}
additionalLink={additionalLink}
setChildReferencesCallback={setChildReferencesCallback}
/>
);
})
.filter(Boolean);
};
const renderSupplementalReferences = (references: SupplementReferences[]) => {
@ -452,10 +509,11 @@ export default function FactChecker() {
conversationId={conversationID}
linkTitle={reference.linkTitle}
setChildReferencesCallback={setChildReferencesCallback}
prefilledResponse={reference.response} />
)
prefilledResponse={reference.response}
/>
);
});
}
};
const renderWebpages = (webpages: WebPage[] | WebPage) => {
if (webpages instanceof Array) {
@ -469,108 +527,138 @@ export default function FactChecker() {
function constructShareUrl() {
const url = new URL(window.location.href);
url.searchParams.set('runId', runId);
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="relative md:fixed h-full">
<SidePanel conversationId={null} uploadedFiles={[]} isMobileWidth={isMobileWidth} />
</div>
<div className={styles.factCheckerContainer}>
<h1
className={`${styles.response} pt-8 md:pt-4 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'>
<Button asChild variant="secondary">
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
Try Another
</Link>
</Button>
<ShareLink
buttonTitle='Share report'
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} />
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 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();
}
</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)}
}}
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="/settings"
className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
subscribed
</a>{" "}
to configure the model.
</h3>
</div>
</div>
}
</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",
message: initialResponse,
context: [],
created: new Date().toISOString(),
onlineContext: {},
}}
isMobileWidth={isMobileWidth}
/>
</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

@ -11,8 +11,8 @@
--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%;
--primary: 24.6 95% 53.1%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
@ -23,33 +23,310 @@
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 209.1 100% 40.8%;
--ring: 24.6 95% 53.1%;
--radius: 0.5rem;
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
--primary-hover: #fee285;
/* Khoj Custom Colors */
--frosted-background-color: 20 13% 95%;
--border-color: #e2e2e2;
/* Imported from Highlight.js */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/
.hljs {
color: #24292e;
background: #ffffff
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #d73a49
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #6f42c1
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #005cc5
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #032f62
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #e36209
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #6a737d
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #22863a
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #24292e
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #005cc5;
font-weight: bold
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #735c0f
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #24292e;
font-style: italic
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #24292e;
font-weight: bold
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #22863a;
background-color: #f0fff4
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #b31d28;
background-color: #ffeef0
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}
}
.dark {
--background: 224 71.4% 4.1%;
--background: 0 0% 14%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card: 0 0% 14%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover: 0 0% 14%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--secondary: 0 0% 9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted: 0 0% 9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent: 0 0% 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%;
--border: 0 0% 9%;
--input: 0 0% 9%;
--ring: 20.5 90.2% 48.2%;
--font-family: "Noto Sans", "Noto Sans Arabic", sans-serif !important;
/* Imported from highlight.js */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/
.hljs {
color: #c9d1d9;
background: #0d1117
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #ff7b72
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #d2a8ff
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #79c0ff
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #a5d6ff
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #ffa657
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #8b949e
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #7ee787
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #c9d1d9
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #1f6feb;
font-weight: bold
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #f2cc60
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #c9d1d9;
font-style: italic
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #c9d1d9;
font-weight: bold
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #aff5b4;
background-color: #033a16
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffdcd7;
background-color: #67060c
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}
}
}

View file

@ -5,18 +5,34 @@ import "./globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI",
description: "An AI copilot for your second brain",
title: "Khoj AI - Home",
description: "Your Second Brain.",
icons: {
icon: "/static/favicon.ico",
},
manifest: "/static/khoj.webmanifest",
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
media-src * blob:;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -1,230 +1,124 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
div.main {
height: 100dvh;
color: hsla(var(--foreground));
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
div.suggestions {
overflow-x: none;
height: fit-content;
padding: 10px;
white-space: nowrap;
gap: 1rem;
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
div.inputBox {
border: 1px solid var(--border-color);
margin-bottom: 20px;
align-content: center;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
max-width: 100%;
width: var(--max-width);
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
text-wrap: balance;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: "";
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo {
position: relative;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
input.inputBox {
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
div.chatBodyFull {
display: grid;
grid-template-columns: 1fr;
height: 100%;
}
button.inputBox {
border: none;
outline: none;
background-color: transparent;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem;
background: linear-gradient(var(--calm-green), var(--calm-blue));
}
div.chatBody {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
}
.inputBox {
color: hsla(var(--foreground));
}
div.chatLayout {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
div.chatBox {
display: grid;
}
div.titleBar {
display: grid;
grid-template-columns: 1fr auto;
}
div.chatBoxBody {
display: grid;
height: 100%;
margin: auto;
}
div.homeGreetings {
display: grid;
height: 100%;
margin: auto;
grid-template-rows: 1fr 2fr;
}
div.sidePanel {
position: fixed;
height: 100%;
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBoxBody {
width: 100%;
grid-template-rows: auto;
}
div.sidePanel {
position: relative;
}
div.chatBox {
padding: 0;
height: 100%;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
div.homeGreetings {
grid-template-rows: auto 1fr;
}
}

View file

@ -1,9 +1,385 @@
"use client";
import "./globals.css";
import styles from "./page.module.css";
import "katex/dist/katex.min.css";
import React, { useEffect, useState } from "react";
import useSWR from "swr";
import Image from "next/image";
import { ArrowCounterClockwise, ClockCounterClockwise } from "@phosphor-icons/react";
import { Card, CardTitle } from "@/components/ui/card";
import SuggestionCard from "@/app/components/suggestions/suggestionCard";
import SidePanel from "@/app/components/sidePanel/chatHistorySidePanel";
import Loading from "@/app/components/loading/loading";
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
import { Suggestion, suggestionsData } from "@/app/components/suggestions/suggestionsData";
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
import { useAuthenticatedData, UserConfig, useUserConfig } from "@/app/common/auth";
import { convertColorToBorderClass } from "@/app/common/colorUtils";
import { getIconFromIconName } from "@/app/common/iconUtils";
import { AgentData } from "@/app/agents/page";
import { createNewConversation } from "./common/chatFunctions";
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
onConversationIdChange?: (conversationId: string) => void;
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
isLoggedIn: boolean;
userConfig: UserConfig | null;
isLoadingUserConfig: boolean;
}
function ChatBodyData(props: ChatBodyDataProps) {
const [message, setMessage] = useState("");
const [processingMessage, setProcessingMessage] = useState(false);
const [greeting, setGreeting] = useState("");
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
const [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
const [agents, setAgents] = useState<AgentData[]>([]);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const onConversationIdChange = props.onConversationIdChange;
const agentsFetcher = () =>
window
.fetch("/api/agents")
.then((res) => res.json())
.catch((err) => console.log(err));
const { data: agentsData, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
revalidateOnFocus: false,
});
function shuffleAndSetOptions() {
const shuffled = [...suggestionsData].sort(() => 0.5 - Math.random());
setShuffledOptions(shuffled.slice(0, 3));
}
useEffect(() => {
if (props.isLoadingUserConfig) return;
// Get today's day
const today = new Date();
const day = today.getDay();
const timeOfDay =
today.getHours() >= 17 || today.getHours() < 4
? "evening"
: today.getHours() >= 12
? "afternoon"
: "morning";
const nameSuffix = props.userConfig?.given_name ? `, ${props.userConfig?.given_name}` : "";
const greetings = [
`What would you like to get done${nameSuffix}?`,
`Hey${nameSuffix}! How can I help?`,
`Good ${timeOfDay}${nameSuffix}! What's on your mind?`,
`Ready to breeze through your ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]}?`,
`Want help navigating your ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]} workload?`,
];
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
setGreeting(greeting);
}, [props.isLoadingUserConfig, props.userConfig]);
useEffect(() => {
if (props.chatOptionsData) {
shuffleAndSetOptions();
}
}, [props.chatOptionsData]);
useEffect(() => {
const nSlice = props.isMobileWidth ? 2 : 4;
const shuffledAgents = agentsData ? [...agentsData].sort(() => 0.5 - Math.random()) : [];
const agents = agentsData ? [agentsData[0]] : []; // Always add the first/default agent.
shuffledAgents.slice(0, nSlice - 1).forEach((agent) => {
if (!agents.find((a) => a.slug === agent.slug)) {
agents.push(agent);
}
});
setAgents(agents);
//generate colored icons for the selected agents
const agentIcons = agents.map(
(agent) =>
getIconFromIconName(agent.icon, agent.color) || (
<Image
key={agent.name}
src={agent.avatar}
alt={agent.name}
width={50}
height={50}
/>
),
);
setAgentIcons(agentIcons);
}, [agentsData, props.isMobileWidth]);
function shuffleSuggestionsCards() {
shuffleAndSetOptions();
}
useEffect(() => {
const processMessage = async () => {
if (message && !processingMessage) {
setProcessingMessage(true);
try {
const newConversationId = await createNewConversation(selectedAgent || "khoj");
onConversationIdChange?.(newConversationId);
window.location.href = `/chat?conversationId=${newConversationId}`;
localStorage.setItem("message", message);
} catch (error) {
console.error("Error creating new conversation:", error);
setProcessingMessage(false);
}
setMessage("");
}
};
processMessage();
if (message) {
setProcessingMessage(true);
}
}, [selectedAgent, message, processingMessage, onConversationIdChange]);
function fillArea(link: string, type: string, prompt: string) {
if (!link) {
let message_str = "";
prompt = prompt.charAt(0).toLowerCase() + prompt.slice(1);
if (type === "Online Search") {
message_str = "/online " + prompt;
} else if (type === "Paint") {
message_str = "/image " + prompt;
} else {
message_str = prompt;
}
// Get the textarea element
const message_area = document.getElementById("message") as HTMLTextAreaElement;
if (message_area) {
// Update the value directly
message_area.value = message_str;
setMessage(message_str);
}
}
}
return (
<div className={`${styles.homeGreetings} w-full md:w-auto`}>
{showLoginPrompt && (
<LoginPrompt
onOpenChange={setShowLoginPrompt}
loginRedirectMessage={"Login to your second brain"}
/>
)}
<div className={`w-full text-center justify-end content-end`}>
<div className="items-center">
<h1 className="text-2xl md:text-3xl text-center w-fit pb-6 px-4 mx-auto">
{greeting}
</h1>
</div>
{!props.isMobileWidth && (
<div className="flex pb-6 gap-2 items-center justify-center">
{agentIcons.map((icon, index) => (
<Card
key={`${index}-${agents[index].slug}`}
className={`${
selectedAgent === agents[index].slug
? convertColorToBorderClass(agents[index].color)
: "border-stone-100 dark:border-neutral-700 text-muted-foreground"
}
hover:cursor-pointer rounded-lg px-2 py-2`}
>
<CardTitle
className="text-center text-md font-medium flex justify-center items-center"
onClick={() => setSelectedAgent(agents[index].slug)}
>
{icon} {agents[index].name}
</CardTitle>
</Card>
))}
<Card
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
onClick={() => (window.location.href = "/agents")}
>
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">
See All
</CardTitle>
</Card>
</div>
)}
</div>
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-fit"}`}>
{!props.isMobileWidth && (
<div
className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}
>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={null}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles}
/>
</div>
)}
<div
className={`${styles.suggestions} w-full ${props.isMobileWidth ? "grid" : "flex flex-row"} justify-center items-center`}
>
{shuffledOptions.map((suggestion, index) => (
<div
key={`${suggestion.type} ${suggestion.description}`}
onClick={(event) => {
if (props.isLoggedIn) {
fillArea(
suggestion.link,
suggestion.type,
suggestion.description,
);
} else {
event.preventDefault();
event.stopPropagation();
setShowLoginPrompt(true);
}
}}
>
<SuggestionCard
key={suggestion.type + Math.random()}
title={suggestion.type}
body={suggestion.description}
link={suggestion.link}
color={suggestion.color}
/>
</div>
))}
</div>
<div className="flex items-center justify-center margin-auto">
<button
onClick={shuffleSuggestionsCards}
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700"
>
More Ideas <ArrowCounterClockwise className="h-4 w-4 inline" />
</button>
</div>
</div>
{props.isMobileWidth && (
<>
<div
className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}
>
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
{agentIcons.map((icon, index) => (
<Card
key={`${index}-${agents[index].slug}`}
className={`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : "border-muted text-muted-foreground"} hover:cursor-pointer`}
>
<CardTitle
className="text-center text-xs font-medium flex justify-center items-center px-1.5 py-1"
onClick={() => setSelectedAgent(agents[index].slug)}
>
{icon} {agents[index].name}
</CardTitle>
</Card>
))}
<Card
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
onClick={() => (window.location.href = "/agents")}
>
<CardTitle
className={`text-center ${props.isMobileWidth ? "text-xs" : "text-md"} font-normal flex justify-center items-center px-1.5 py-2`}
>
See All
</CardTitle>
</Card>
</div>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={null}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles}
/>
</div>
</>
)}
</div>
);
}
export default function Home() {
return (
<main className={`${styles.main} text-3xl font-bold underline`}>
Hi, Khoj here.
</main>
);
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [conversationId, setConversationID] = useState<string | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const { userConfig: initialUserConfig, isLoadingUserConfig } = useUserConfig(true);
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
const authenticatedData = useAuthenticatedData();
const handleConversationIdChange = (newConversationId: string) => {
setConversationID(newConversationId);
};
useEffect(() => {
setUserConfig(initialUserConfig);
}, [initialUserConfig]);
useEffect(() => {
fetch("/api/chat/options")
.then((response) => response.json())
.then((data: ChatOptions) => {
setLoading(false);
if (data) {
setChatOptionsData(data);
}
})
.catch((err) => {
console.error(err);
return;
});
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 786);
});
}, []);
if (isLoading) {
return <Loading />;
}
return (
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>Khoj AI - Your Second Brain</title>
<div className={`${styles.sidePanel}`}>
<SidePanel
conversationId={conversationId}
uploadedFiles={uploadedFiles}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`${styles.chatBox}`}>
<div className={`${styles.chatBoxBody}`}>
<ChatBodyData
isLoggedIn={authenticatedData !== null}
chatOptionsData={chatOptionsData}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth}
onConversationIdChange={handleConversationIdChange}
userConfig={userConfig}
isLoadingUserConfig={isLoadingUserConfig}
/>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "../globals.css";
export const metadata: Metadata = {
title: "Khoj AI - Search",
description:
"Find anything in documents you've shared with Khoj using natural language queries.",
icons: {
icon: "/static/favicon.ico",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <div>{children}</div>;
}

View file

@ -0,0 +1,339 @@
"use client";
import { Input } from "@/components/ui/input";
import { useAuthenticatedData } from "../common/auth";
import { useEffect, useRef, useState } from "react";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import styles from "./search.module.css";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ArrowLeft,
ArrowRight,
FileDashed,
FileMagnifyingGlass,
Folder,
FolderOpen,
GithubLogo,
Lightbulb,
LinkSimple,
MagnifyingGlass,
NoteBlank,
NotionLogo,
} from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
interface AdditionalData {
file: string;
source: string;
compiled: string;
heading: string;
}
interface SearchResult {
type: string;
additional: AdditionalData;
entry: string;
score: number;
"corpus-id": string;
}
function getNoteTypeIcon(source: string) {
if (source === "notion") {
return <NotionLogo className="text-muted-foreground" />;
}
if (source === "github") {
return <GithubLogo className="text-muted-foreground" />;
}
return <NoteBlank className="text-muted-foreground" />;
}
const naturalLanguageSearchQueryExamples = [
"What does the paper say about climate change?",
"Making a cappuccino at home",
"Benefits of eating mangoes",
"How to plan a wedding on a budget",
"Appointment with Dr. Makinde on 12th August",
"Class notes lecture 3 on quantum mechanics",
"Painting concepts for acrylics",
"Abstract from the paper attention is all you need",
"Climbing Everest without oxygen",
"Solving a rubik's cube in 30 seconds",
"Facts about the planet Mars",
"How to make a website using React",
"Fish at the bottom of the ocean",
"Fish farming Kenya 2021",
"How to make a cake without an oven",
"Installing a solar panel at home",
];
interface NoteResultProps {
note: SearchResult;
setFocusSearchResult: (note: SearchResult) => void;
}
function Note(props: NoteResultProps) {
const note = props.note;
const isFileNameURL = (note.additional.file || "").startsWith("http");
const fileName = isFileNameURL
? note.additional.heading
: note.additional.file.split("/").pop();
return (
<Card className="bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4">
<CardHeader>
<CardDescription className="p-1 border-muted border w-fit rounded-lg mb-2">
{getNoteTypeIcon(note.additional.source)}
</CardDescription>
<CardTitle>{fileName}</CardTitle>
</CardHeader>
<CardContent>
<div className="line-clamp-4 text-muted-foreground">{note.entry}</div>
<Button
onClick={() => props.setFocusSearchResult(note)}
variant={"ghost"}
className="p-0 mt-2 text-orange-400 hover:bg-inherit"
>
See content
<ArrowRight className="inline ml-2" />
</Button>
</CardContent>
<CardFooter>
{isFileNameURL ? (
<a
href={note.additional.file}
target="_blank"
className="underline text-sm bg-muted p-1 rounded-lg text-muted-foreground"
>
<LinkSimple className="inline m-2" />
{note.additional.file}
</a>
) : (
<div className="bg-muted p-1 text-sm rounded-lg text-muted-foreground">
<FolderOpen className="inline m-2" />
{note.additional.file}
</div>
)}
</CardFooter>
</Card>
);
}
function focusNote(note: SearchResult) {
const isFileNameURL = (note.additional.file || "").startsWith("http");
const fileName = isFileNameURL
? note.additional.heading
: note.additional.file.split("/").pop();
return (
<Card className="bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4">
<CardHeader>
<CardTitle>{fileName}</CardTitle>
</CardHeader>
<CardFooter>
{isFileNameURL ? (
<a
href={note.additional.file}
target="_blank"
className="underline text-sm bg-muted p-3 rounded-lg text-muted-foreground flex items-center gap-2"
>
<LinkSimple className="inline" />
{note.additional.file}
</a>
) : (
<div className="bg-muted p-3 text-sm rounded-lg text-muted-foreground flex items-center gap-2">
<FolderOpen className="inline" />
{note.additional.file}
</div>
)}
</CardFooter>
<CardContent>
<div className="text-m">{note.entry}</div>
</CardContent>
</Card>
);
}
export default function Search() {
const authenticatedData = useAuthenticatedData();
const [searchQuery, setSearchQuery] = useState("");
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [title, setTitle] = useState("Search");
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null);
const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
const [exampleQuery, setExampleQuery] = useState("");
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 786);
});
setExampleQuery(
naturalLanguageSearchQueryExamples[
Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)
],
);
}, []);
useEffect(() => {
setTitle(isMobileWidth ? "" : "Search");
}, [isMobileWidth]);
function search() {
if (searchResultsLoading || !searchQuery.trim()) return;
const apiUrl = `/api/search?q=${encodeURIComponent(searchQuery)}&client=web`;
fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
setSearchResults(data);
setSearchResultsLoading(false);
})
.catch((error) => {
console.error("Error:", error);
});
}
useEffect(() => {
if (!searchQuery.trim()) {
return;
}
setFocusSearchResult(null);
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (searchQuery.trim()) {
searchTimeoutRef.current = setTimeout(() => {
search();
}, 750); // 1000 milliseconds = 1 second
}
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, [searchQuery]);
return (
<div>
<div className={`h-full ${styles.sidePanel}`}>
<SidePanel conversationId={null} uploadedFiles={[]} isMobileWidth={isMobileWidth} />
</div>
<div className={`${styles.searchLayout}`}>
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
<div className="p-4 md:w-3/4 sm:w-full mx-auto">
<div className="flex justify-between items-center border-2 border-muted p-2 gap-4 rounded-lg">
<MagnifyingGlass className="inline m-2 h-4 w-4" />
<Input
autoFocus={true}
className="border-none"
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={(e) => e.key === "Enter" && search()}
type="search"
placeholder="Search Documents"
/>
<button className="px-4 rounded" onClick={() => search()}>
Find
</button>
</div>
{focusSearchResult && (
<div className="mt-4">
<Button
onClick={() => setFocusSearchResult(null)}
className="mb-4"
variant={"outline"}
>
<ArrowLeft className="inline mr-2" />
Back
</Button>
{focusNote(focusSearchResult)}
</div>
)}
{!focusSearchResult && searchResults && searchResults.length > 0 && (
<div className="mt-4 max-w-[92vw] break-all">
<ScrollArea className="h-[80vh]">
{searchResults.map((result, index) => {
return (
<Note
key={result["corpus-id"]}
note={result}
setFocusSearchResult={setFocusSearchResult}
/>
);
})}
</ScrollArea>
</div>
)}
{searchResults == null && (
<Card className="flex flex-col items-center justify-center border-none shadow-none">
<CardHeader className="flex flex-col items-center justify-center">
<CardDescription className="border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4">
<FileMagnifyingGlass
weight="fill"
className="text-muted-foreground h-10 w-10"
/>
</CardDescription>
<CardTitle className="text-center">
Search across your documents
</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground items-center justify-center text-center flex">
<Lightbulb className="inline mr-2" /> {exampleQuery}
</CardContent>
</Card>
)}
{searchResults && searchResults.length === 0 && (
<Card className="flex flex-col items-center justify-center border-none shadow-none">
<CardHeader className="flex flex-col items-center justify-center">
<CardDescription className="border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4">
<FileDashed
weight="fill"
className="text-muted-foreground h-10 w-10"
/>
</CardDescription>
<CardTitle className="text-center">
No documents found
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-muted-foreground items-center justify-center text-center flex">
To use search, upload your docs to your account.
</div>
<Link
href="https://docs.khoj.dev/data-sources/share_your_data"
className="no-underline"
>
<div className="mt-4 text-center text-secondary-foreground bg-secondary w-fit m-auto p-2 rounded-lg">
Learn More
</div>
</Link>
</CardContent>
</Card>
)}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,22 @@
div.searchLayout {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
height: 100vh;
}
div.sidePanel {
position: fixed;
height: 100%;
}
@media screen and (max-width: 768px) {
div.searchLayout {
gap: 0;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View file

@ -0,0 +1,40 @@
import type { Metadata } from "next";
import { Noto_Sans } from "next/font/google";
import "../globals.css";
import { Toaster } from "@/components/ui/toaster";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Settings",
description: "Configure Khoj to get personalized, deeper assistance.",
icons: {
icon: "/static/favicon.ico",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>
{children}
<Toaster />
</body>
</html>
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
div.page {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
height: 100vh;
color: hsla(var(--foreground));
}
div.contentBody {
display: grid;
margin: auto;
margin-left: 20vw;
margin-top: 1rem;
}
div.phoneInput {
padding: 0rem;
}
div.sidePanel {
position: fixed;
height: 100%;
}
div.phoneInput input {
width: 100%;
padding: 0.5rem;
border: 1px solid hsla(var(--border));
border-radius: 0.25rem;
}
@media screen and (max-width: 768px) {
div.sidePanel {
position: relative;
height: 100%;
}
div.contentBody {
margin-left: 0;
margin-top: 0;
}
}

View file

@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Noto_Sans } from "next/font/google";
import "../../globals.css";
const inter = Noto_Sans({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Khoj AI - Chat",
description: "Use this page to view a chat with Khoj AI.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'self' https://assets.khoj.dev;
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
child-src 'none';
object-src 'none';"
></meta>
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -0,0 +1,290 @@
"use client";
import styles from "./sharedChat.module.css";
import React, { Suspense, useEffect, useRef, useState } from "react";
import SidePanel from "../../components/sidePanel/chatHistorySidePanel";
import ChatHistory from "../../components/chatHistory/chatHistory";
import NavMenu from "../../components/navMenu/navMenu";
import Loading from "../../components/loading/loading";
import "katex/dist/katex.min.css";
import { useIPLocationData, welcomeConsole } from "../../common/utils";
import { useAuthenticatedData } from "@/app/common/auth";
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
import { StreamMessage } from "@/app/components/chatMessage/chatMessage";
import { processMessageChunk } from "@/app/common/chatFunctions";
import { AgentData } from "@/app/agents/page";
interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
setTitle: (title: string) => void;
setUploadedFiles: (files: string[]) => void;
isMobileWidth?: boolean;
publicConversationSlug: string;
streamedMessages: StreamMessage[];
isLoggedIn: boolean;
conversationId?: string;
setQueryToProcess: (query: string) => void;
}
function ChatBodyData(props: ChatBodyDataProps) {
const [message, setMessage] = useState("");
const [processingMessage, setProcessingMessage] = useState(false);
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
const setQueryToProcess = props.setQueryToProcess;
const streamedMessages = props.streamedMessages;
useEffect(() => {
if (message) {
setProcessingMessage(true);
setQueryToProcess(message);
}
}, [message, setQueryToProcess]);
useEffect(() => {
if (
streamedMessages &&
streamedMessages.length > 0 &&
streamedMessages[streamedMessages.length - 1].completed
) {
setProcessingMessage(false);
} else {
setMessage("");
}
}, [streamedMessages]);
if (!props.publicConversationSlug && !props.conversationId) {
return <div className={styles.suggestions}>Whoops, nothing to see here!</div>;
}
return (
<>
<div className={false ? styles.chatBody : styles.chatBodyFull}>
<ChatHistory
publicConversationSlug={props.publicConversationSlug}
conversationId={props.conversationId || ""}
setAgent={setAgentMetadata}
setTitle={props.setTitle}
pendingMessage={processingMessage ? message : ""}
incomingMessages={props.streamedMessages}
/>
</div>
<div
className={`${styles.inputBox} shadow-md bg-background align-middle items-center justify-center px-3`}
>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={props.conversationId}
agentColor={agentMetadata?.color}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles}
/>
</div>
</>
);
}
export default function SharedChat() {
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState("Khoj AI - Chat");
const [conversationId, setConversationID] = useState<string | undefined>(undefined);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>("");
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [paramSlug, setParamSlug] = useState<string | undefined>(undefined);
const locationData = useIPLocationData();
const authenticatedData = useAuthenticatedData();
useEffect(() => {
fetch("/api/chat/options")
.then((response) => response.json())
.then((data: ChatOptions) => {
setLoading(false);
// Render chat options, if any
if (data) {
setChatOptionsData(data);
}
})
.catch((err) => {
console.error(err);
return;
});
welcomeConsole();
setIsMobileWidth(window.innerWidth < 786);
window.addEventListener("resize", () => {
setIsMobileWidth(window.innerWidth < 786);
});
setParamSlug(window.location.pathname.split("/").pop() || "");
}, []);
useEffect(() => {
if (queryToProcess && !conversationId) {
// If the user has not yet started conversing in the chat, create a new conversation
fetch(`/api/chat/share/fork?public_conversation_slug=${paramSlug}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
setConversationID(data.conversation_id);
})
.catch((err) => {
console.error(err);
return;
});
return;
}
if (queryToProcess) {
// Add a new object to the state
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: new Date().toISOString(),
rawQuery: queryToProcess || "",
};
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
}
}, [queryToProcess, conversationId, paramSlug]);
useEffect(() => {
if (processQuerySignal) {
chat();
}
}, [processQuerySignal]);
async function readChatStream(response: Response) {
if (!response.ok) throw new Error(response.statusText);
if (!response.body) throw new Error("Response body is null");
const reader = response.body.getReader();
const decoder = new TextDecoder();
const eventDelimiter = "␃🔚␗";
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
setQueryToProcess("");
setProcessQuerySignal(false);
break;
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
let newEventIndex;
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
const event = buffer.slice(0, newEventIndex);
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
if (event) {
const currentMessage = messages.find((message) => !message.completed);
if (!currentMessage) {
console.error("No current message found");
return;
}
processMessageChunk(event, currentMessage);
setMessages([...messages]);
}
}
}
}
async function chat() {
if (!queryToProcess || !conversationId) return;
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`;
if (locationData) {
chatAPI += `&region=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`;
}
const response = await fetch(chatAPI);
try {
await readChatStream(response);
} catch (err) {
console.log(err);
}
}
useEffect(() => {
(async () => {
if (conversationId) {
// Add a new object to the state
const newStreamMessage: StreamMessage = {
rawResponse: "",
trainOfThought: [],
context: [],
onlineContext: {},
completed: false,
timestamp: new Date().toISOString(),
rawQuery: queryToProcess || "",
};
setProcessQuerySignal(true);
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
}
})();
}, [conversationId, queryToProcess]);
if (isLoading) {
return <Loading />;
}
if (!paramSlug) {
return <div className={styles.suggestions}>Whoops, nothing to see here!</div>;
}
return (
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>{title}</title>
<div className={styles.sidePanel}>
<SidePanel
conversationId={conversationId ?? null}
uploadedFiles={uploadedFiles}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={styles.chatBox}>
<div className={styles.chatBoxBody}>
<Suspense fallback={<Loading />}>
<ChatBodyData
conversationId={conversationId}
streamedMessages={messages}
setQueryToProcess={setQueryToProcess}
isLoggedIn={authenticatedData !== null}
publicConversationSlug={paramSlug}
chatOptionsData={chatOptionsData}
setTitle={setTitle}
setUploadedFiles={setUploadedFiles}
isMobileWidth={isMobileWidth}
/>
</Suspense>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,128 @@
div.main {
height: 100vh;
color: hsla(var(--foreground));
}
.suggestions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
justify-content: center;
}
div.inputBox {
border: 1px solid var(--border-color);
border-radius: 16px;
margin-bottom: 20px;
gap: 12px;
padding-left: 20px;
padding-right: 20px;
align-content: center;
}
input.inputBox {
border: none;
}
input.inputBox:focus {
outline: none;
background-color: transparent;
}
div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
div.chatBodyFull {
display: grid;
grid-template-columns: 1fr;
height: 100%;
}
button.inputBox {
border: none;
outline: none;
background-color: transparent;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem;
background: linear-gradient(var(--calm-green), var(--calm-blue));
}
div.chatBody {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
}
.inputBox {
color: hsla(var(--foreground));
}
div.chatLayout {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
}
div.chatBox {
display: grid;
height: 100%;
}
div.titleBar {
display: grid;
grid-template-columns: 1fr auto;
}
div.chatBoxBody {
display: grid;
height: 100%;
width: 70%;
margin: auto;
}
div.agentIndicator a {
display: flex;
text-align: center;
align-content: center;
align-items: center;
}
div.agentIndicator {
padding: 10px;
}
@media (max-width: 768px) {
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
height: min-content;
}
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBoxBody {
width: 100%;
}
div.chatBox {
padding: 0;
height: min-content;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
}
}

View file

@ -1,141 +1,117 @@
"use client"
"use client";
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
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"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
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
<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>
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>
<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}
/>
</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"
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
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
<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>
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
<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>
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
<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>
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
<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,
}
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View file

@ -0,0 +1,49 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View file

@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };

View file

@ -1,56 +1,54 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
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"
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",
},
"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",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
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"
({ 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 }
export { Button, buttonVariants };

View file

@ -1,79 +1,56 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
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 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 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 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>
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
<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 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"
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 }
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View file

@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -0,0 +1,145 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className={cn("mr-2 h-4 w-4 shrink-0 opacity-50", className)} />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View file

@ -1,122 +1,107 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
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
<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>
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
<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 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 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>
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
<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>
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
<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,
}
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View file

@ -0,0 +1,100 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
);
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View file

@ -0,0 +1,187 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -0,0 +1,171 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View file

@ -0,0 +1,71 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View file

@ -1,25 +1,24 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
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"
({ 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 }
export { Input };

View file

@ -1,26 +1,21 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
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"
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
"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>
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
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View file

@ -0,0 +1,221 @@
"use client";
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className,
)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
);
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View file

@ -0,0 +1,123 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View file

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
indicatorColor?: string;
}
const Progress = React.forwardRef<React.ElementRef<typeof ProgressPrimitive.Root>, ProgressProps>(
({ className, value, indicatorColor, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className={`h-full w-full flex-1 bg-primary transition-all ${indicatorColor}`}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
),
);
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -0,0 +1,46 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

Some files were not shown because too many files have changed in this diff Show more