mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 21:29:08 +00:00
Checkpoint: Updated sidebar panel with new components
- Add non-functional UI elements for chat, references, feedback buttons, rename/share session, mic, attachment, websocket connection
This commit is contained in:
@@ -7,6 +7,16 @@ 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.agentIndicator {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,18 @@ import { useRef, useEffect, useState } from 'react';
|
||||
|
||||
import ChatMessage, { ChatHistoryData, SingleChatMessage } from '../chatMessage/chatMessage';
|
||||
|
||||
import ReferencePanel, { hasValidReferences} from '../referencePanel/referencePanel';
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
|
||||
import renderMathInElement from 'katex/contrib/auto-render';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/github.css'
|
||||
|
||||
import Loading from '../loading/loading';
|
||||
|
||||
import { Lightbulb } from "@phosphor-icons/react";
|
||||
|
||||
interface ChatResponse {
|
||||
status: string;
|
||||
response: ChatHistoryData;
|
||||
@@ -20,8 +28,7 @@ interface ChatHistory {
|
||||
|
||||
interface ChatHistoryProps {
|
||||
conversationId: string;
|
||||
setReferencePanelData: Function;
|
||||
setShowReferencePanel: Function;
|
||||
setTitle: (title: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +38,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const chatHistoryRef = useRef(null);
|
||||
|
||||
const [showReferencePanel, setShowReferencePanel] = useState(true);
|
||||
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -38,10 +47,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
.then(response => response.json())
|
||||
.then((chatData: ChatResponse) => {
|
||||
setLoading(false);
|
||||
|
||||
// Render chat options, if any
|
||||
if (chatData) {
|
||||
console.log(chatData);
|
||||
setData(chatData.response);
|
||||
props.setTitle(chatData.response.slug);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -55,7 +66,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
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) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
// Call your function here
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
@@ -78,23 +89,50 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <h2>🌀 Loading...</h2>;
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
function constructAgentLink() {
|
||||
if (!data || !data.agent || !data.agent.slug) return `/agents`;
|
||||
return `/agents?agent=${data.agent.slug}`
|
||||
}
|
||||
|
||||
function constructAgentAvatar() {
|
||||
if (!data || !data.agent || !data.agent.avatar) return `/avatar.png`;
|
||||
return data.agent.avatar;
|
||||
}
|
||||
|
||||
function constructAgentName() {
|
||||
if (!data || !data.agent || !data.agent.name) return `Agent`;
|
||||
return data.agent.name;
|
||||
}
|
||||
|
||||
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) => (
|
||||
<ChatMessage
|
||||
key={index}
|
||||
chatMessage={chatMessage}
|
||||
setReferencePanelData={props.setReferencePanelData}
|
||||
setShowReferencePanel={props.setShowReferencePanel}
|
||||
setReferencePanelData={setReferencePanelData}
|
||||
setShowReferencePanel={setShowReferencePanel}
|
||||
customClassName='fullHistory'
|
||||
borderLeftColor='orange-400'
|
||||
/>
|
||||
))}
|
||||
{
|
||||
(hasValidReferences(referencePanelData) && showReferencePanel) &&
|
||||
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||
}
|
||||
<div className={`${styles.agentIndicator}`}>
|
||||
<a className='no-underline mx-2 flex' href={constructAgentLink()} target="_blank" rel="noreferrer">
|
||||
<Lightbulb color='orange' weight='fill' />
|
||||
<span className='text-neutral-600'>{constructAgentName()}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
div.chatMessageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 12px;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
div.chatMessageWrapper {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
div.khojfullHistory {
|
||||
border-color: var(--border-color);
|
||||
border-width: 1px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
div.youfullHistory {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
div.chatMessageContainer.youfullHistory {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
div.you {
|
||||
color: var(--frosted-background-color);
|
||||
background-color: var(--intense-green);
|
||||
background-color: var(--frosted-background-color);
|
||||
align-self: flex-end;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
div.khoj {
|
||||
@@ -15,6 +37,11 @@ div.khoj {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
div.khojChatMessage {
|
||||
padding-top: 8px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
div.chatMessageContainer img {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -42,21 +69,25 @@ div.chatFooter {
|
||||
div.chatButtons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border: var(--border-color) 1px solid;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
bottom: -28px;
|
||||
background-color: hsla(var(--background));
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -70,7 +101,6 @@ button.codeCopyButton {
|
||||
}
|
||||
|
||||
button.codeCopyButton:hover {
|
||||
background-color: var(--intense-green);
|
||||
color: var(--frosted-background-color);
|
||||
}
|
||||
|
||||
@@ -78,3 +108,10 @@ div.feedbackButtons img,
|
||||
button.copyButton img {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
div.youfullHistory {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'highlight.js/styles/github.css'
|
||||
|
||||
import { hasValidReferences } from '../referencePanel/referencePanel';
|
||||
|
||||
import { ThumbsUp, ThumbsDown, Copy } from '@phosphor-icons/react';
|
||||
|
||||
const md = new markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
@@ -103,31 +105,19 @@ export interface ChatHistoryData {
|
||||
|
||||
function FeedbackButtons() {
|
||||
return (
|
||||
<div className={styles.feedbackButtons}>
|
||||
<div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
|
||||
<button className={styles.thumbsUpButton}>
|
||||
<Image
|
||||
src="/thumbs-up.svg"
|
||||
alt="Thumbs Up"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
<ThumbsUp color='hsl(var(--muted-foreground))' />
|
||||
</button>
|
||||
<button className={styles.thumbsDownButton}>
|
||||
<Image
|
||||
src="/thumbs-down.svg"
|
||||
alt="Thumbs Down"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
<ThumbsDown color='hsl(var(--muted-foreground))' />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function onClickMessage(event: React.MouseEvent<any>, chatMessage: SingleChatMessage, setReferencePanelData: Function, setShowReferencePanel: Function) {
|
||||
event.preventDefault();
|
||||
console.log("Clicked on message", chatMessage);
|
||||
setReferencePanelData(chatMessage);
|
||||
setShowReferencePanel(true);
|
||||
}
|
||||
@@ -136,6 +126,8 @@ interface ChatMessageProps {
|
||||
chatMessage: SingleChatMessage;
|
||||
setReferencePanelData: Function;
|
||||
setShowReferencePanel: Function;
|
||||
customClassName?: string;
|
||||
borderLeftColor?: string;
|
||||
}
|
||||
|
||||
export default function ChatMessage(props: ChatMessageProps) {
|
||||
@@ -204,53 +196,65 @@ export default function ChatMessage(props: ChatMessageProps) {
|
||||
|
||||
let referencesValid = hasValidReferences(props.chatMessage);
|
||||
|
||||
function constructClasses(chatMessage: SingleChatMessage) {
|
||||
let classes = [styles.chatMessageContainer];
|
||||
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-orange-500 border-l-${props.borderLeftColor}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.chatMessageContainer} ${styles[props.chatMessage.by]}`}
|
||||
className={constructClasses(props.chatMessage)}
|
||||
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);
|
||||
}}>
|
||||
<div className={chatMessageWrapperClasses(props.chatMessage)}>
|
||||
<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}>
|
||||
{
|
||||
copySuccess ?
|
||||
<Image
|
||||
src="/copy-button-success.svg"
|
||||
alt="Checkmark"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
: <Image
|
||||
src="/copy-button.svg"
|
||||
alt="Copy"
|
||||
width={24}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
referencesValid &&
|
||||
<div className={styles.referenceButton}>
|
||||
<button onClick={(event) => onClickMessage(event, props.chatMessage, props.setReferencePanelData, props.setShowReferencePanel)}>
|
||||
References
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
{
|
||||
props.chatMessage.by === "khoj" && <FeedbackButtons />
|
||||
}
|
||||
<button className={`${styles.copyButton}`} onClick={() => {
|
||||
navigator.clipboard.writeText(props.chatMessage.message);
|
||||
setCopySuccess(true);
|
||||
}}>
|
||||
{
|
||||
copySuccess ?
|
||||
<Copy color='green' />
|
||||
: <Copy color='hsl(var(--muted-foreground))' />
|
||||
}
|
||||
</button>
|
||||
{
|
||||
props.chatMessage.by === "khoj" && <FeedbackButtons />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
24
src/interface/web/app/components/loading/loading.module.css
Normal file
24
src/interface/web/app/components/loading/loading.module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
--c: conic-gradient(from -90deg, hsla(var(--secondary)) 90deg, #0000 0);
|
||||
background: var(--c), var(--c);
|
||||
background-size: 40% 40%;
|
||||
animation: l19 1s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes l19 {
|
||||
|
||||
0%,
|
||||
10% {
|
||||
background-position: 0 0, 0 calc(100%/3)
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 0 0, calc(100%/3) calc(100%/3)
|
||||
}
|
||||
|
||||
90%,
|
||||
100% {
|
||||
background-position: 0 0, calc(100%/3) 0
|
||||
}
|
||||
}
|
||||
26
src/interface/web/app/components/loading/loading.tsx
Normal file
26
src/interface/web/app/components/loading/loading.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import styles from './loading.module.css';
|
||||
|
||||
export default function Loading() {
|
||||
// return (
|
||||
// <div className={`${styles.loading} h-[100vh] flex items-center justify-center`}>
|
||||
// <button type="button" className="bg-indigo-500" disabled>
|
||||
// Loading...
|
||||
// <span className="relative flex h-3 w-3">
|
||||
// <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
|
||||
// <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
|
||||
// </span>
|
||||
// </button>
|
||||
// </div>
|
||||
// )
|
||||
return (
|
||||
<div className={`${styles.loader} h-[100vh] flex items-center justify-center`}></div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-[100vh] flex items-center justify-center">
|
||||
<h2 className="text-4xl text-black animate-bounce">
|
||||
Loading...
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,26 +11,19 @@ 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;
|
||||
padding-left: 12px;
|
||||
padding-right: 32px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
justify-content: space-between;
|
||||
align-content: space-evenly;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
div.titleBar menu {
|
||||
@@ -75,14 +68,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 +76,8 @@ div.settingsMenuUsername {
|
||||
div.settingsMenuOptions {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
div.titleBar {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,125 @@
|
||||
'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 { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
Menubar,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarSeparator,
|
||||
MenubarTrigger,
|
||||
} from "@/components/ui/menubar";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
|
||||
interface NavMenuProps {
|
||||
selected: string;
|
||||
showLogo?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function SettingsMenu(props: UserProfile) {
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
export default function NavMenu(props: NavMenuProps) {
|
||||
|
||||
let userData = useAuthenticatedData();
|
||||
const userData = useAuthenticatedData();
|
||||
const [displayTitle, setDisplayTitle] = useState<string>(props.title || props.selected.toUpperCase());
|
||||
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
setDisplayTitle(props.title || props.selected.toUpperCase());
|
||||
|
||||
}, [props.title]);
|
||||
|
||||
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 className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
|
||||
{displayTitle && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`} >{displayTitle}</h2>}
|
||||
</div>
|
||||
{
|
||||
isMobileWidth ?
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>=</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>Chat</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>Agents</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>Automations</Link>
|
||||
</DropdownMenuItem>
|
||||
{userData && <>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Profile</DropdownMenuLabel>
|
||||
<DropdownMenuItem>
|
||||
<Link href="/config">Settings</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href="https://docs.khoj.dev">Help</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href="/auth/logout">Logout</Link>
|
||||
</DropdownMenuItem>
|
||||
</>}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
:
|
||||
<Menubar className='items-top'>
|
||||
<MenubarMenu>
|
||||
<Link href='/chat' className={`${props.selected.toLowerCase() === 'chat' ? styles.selected : ''} hover:bg-background`}>
|
||||
<MenubarTrigger>Chat</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<Link href='/agents' className={`${props.selected.toLowerCase() === 'agent' ? styles.selected : ''} hover:bg-background`}>
|
||||
<MenubarTrigger>Agents</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
<Link href='/automations' className={`${props.selected.toLowerCase() === 'automations' ? styles.selected : ''} hover:bg-background`}>
|
||||
<MenubarTrigger>Automations</MenubarTrigger>
|
||||
</Link>
|
||||
</MenubarMenu>
|
||||
{userData &&
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>Profile</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem>
|
||||
<Link href="/config">
|
||||
Settings
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem>
|
||||
<Link href="https://docs.khoj.dev">
|
||||
Help
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem>
|
||||
<Link href="/auth/logout">
|
||||
Logout
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
}
|
||||
</Menubar>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ div.panel {
|
||||
}
|
||||
|
||||
div.panel a {
|
||||
color: var(--intense-green);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -29,3 +28,13 @@ div.singleReference {
|
||||
background-color: var(--frosted-background-color);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
div.panel {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
div.singleReference {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function hasValidReferences(referencePanelData: SingleChatMessage | null)
|
||||
(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))
|
||||
(onlineContextData.webpages && onlineContextData.webpages.length > 0) || onlineContextData.answerBox || onlineContextData.peopleAlsoAsk || onlineContextData.knowledgeGraph || onlineContextData.organic ))
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -98,6 +98,7 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
|
||||
const answerBox = props.onlineContext.answerBox;
|
||||
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
|
||||
const knowledgeGraph = props.onlineContext.knowledgeGraph;
|
||||
const organic = props.onlineContext.organic;
|
||||
|
||||
return (
|
||||
<div className={styles.singleReference}>
|
||||
@@ -126,6 +127,24 @@ function OnlineReferences(props: { onlineContext: OnlineContextData, query: stri
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
organic && organic.map((organicData, index) => {
|
||||
return (
|
||||
<div className={styles.onlineReference} key={index}>
|
||||
<div className={styles.onlineReferenceTitle}>
|
||||
<a href={organicData.link} target="_blank" rel="noreferrer">
|
||||
{organicData.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.onlineReferenceContent}>
|
||||
<div>
|
||||
{organicData.snippet}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
|
||||
return (
|
||||
|
||||
@@ -5,69 +5,254 @@ import styles from "./sidePanel.module.css";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { UserProfile } from "@/app/common/auth";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
interface ChatHistory {
|
||||
conversation_id: string;
|
||||
slug: string;
|
||||
agent_name: string;
|
||||
agent_avatar: string;
|
||||
compressed: boolean;
|
||||
}
|
||||
|
||||
function ChatSession(prop: ChatHistory) {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { Pencil, Trash, Share } from "@phosphor-icons/react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface GroupedChatHistory {
|
||||
[key: string]: ChatHistory[];
|
||||
}
|
||||
|
||||
function renameConversation(conversationId: string, newTitle: string) {
|
||||
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
|
||||
|
||||
fetch(editUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function shareConversation(conversationId: string) {
|
||||
const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`;
|
||||
|
||||
fetch(shareUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteConversation(conversationId: string) {
|
||||
const deleteUrl = `/api/chat/delete?client=web&conversation_id=${conversationId}`;
|
||||
|
||||
fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
interface ChatSessionActionMenuProps {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
||||
|
||||
return (
|
||||
<div key={prop.conversation_id} className={styles.session}>
|
||||
<Link href={`/chat?conversationId=${prop.conversation_id}`}>
|
||||
<p className={styles.session}>{prop.slug || "New Conversation 🌱"}</p>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>:</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => renameConversation(props.conversationId, 'New Title')}>
|
||||
<Pencil className="mr-2 h-4 w-4" />Rename
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => shareConversation(props.conversationId)}>
|
||||
<Share className="mr-2 h-4 w-4" />Share
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400" variant={'ghost'} onClick={() => deleteConversation(props.conversationId)}>
|
||||
<Trash className="mr-2 h-4 w-4" />Delete
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatSession(props: ChatHistory) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
key={props.conversation_id}
|
||||
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''}`}>
|
||||
<Link href={`/chat?conversationId=${props.conversation_id}`}>
|
||||
<p className={styles.session}>{props.slug || "New Conversation 🌱"}</p>
|
||||
</Link>
|
||||
<ChatSessionActionMenu conversationId={props.conversation_id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatSessionsModalProps {
|
||||
data: ChatHistory[];
|
||||
setIsExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
data: GroupedChatHistory | null;
|
||||
}
|
||||
|
||||
function ChatSessionsModal({data, setIsExpanded}: ChatSessionsModalProps) {
|
||||
function ChatSessionsModal({ data }: ChatSessionsModalProps) {
|
||||
return (
|
||||
<div className={styles.modalSessionsList}>
|
||||
<div className={styles.content}>
|
||||
{data.map((chatHistory) => (
|
||||
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
||||
))}
|
||||
<button className={styles.showMoreButton} onClick={() => setIsExpanded(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className="flex text-left text-medium text-gray-500 hover:text-gray-900 cursor-pointer">
|
||||
Show All
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>All Conversations</DialogTitle>
|
||||
<DialogDescription>
|
||||
<ScrollArea className="h-[500px] w-[450px] rounded-md border p-4">
|
||||
{data && Object.keys(data).map((agentName) => (
|
||||
<div key={agentName}>
|
||||
<h3 className={`grid grid-flow-col auto-cols-max gap-2`}>
|
||||
<img src={data[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
|
||||
{agentName}
|
||||
</h3>
|
||||
{data[agentName].map((chatHistory) => (
|
||||
<ChatSession
|
||||
compressed={false}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={chatHistory.conversation_id}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SidePanel() {
|
||||
const fetchChatHistory = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const useChatHistoryRecentFetchRequest = (url: string) => {
|
||||
const { data, error } = useSWR<ChatHistory[]>(url, fetchChatHistory);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
};
|
||||
|
||||
interface SidePanelProps {
|
||||
webSocketConnected?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export default function SidePanel(props: SidePanelProps) {
|
||||
|
||||
const [data, setData] = useState<ChatHistory[] | null>(null);
|
||||
const [dataToShow, setDataToShow] = useState<ChatHistory[] | null>(null);
|
||||
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
|
||||
const [subsetOrganizedData, setSubsetOrganizedData] = useState<GroupedChatHistory | null>(null);
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(false);
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { data: chatHistory } = useChatHistoryRecentFetchRequest('/api/chat/sessions');
|
||||
|
||||
fetch('/api/chat/sessions', { method: 'GET' })
|
||||
.then(response => response.json())
|
||||
.then((data: ChatHistory[]) => {
|
||||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
setData(data);
|
||||
setDataToShow(data.slice(0, 5));
|
||||
useEffect(() => {
|
||||
if (chatHistory) {
|
||||
setData(chatHistory);
|
||||
|
||||
const groupedData: GroupedChatHistory = {};
|
||||
const subsetOrganizedData: GroupedChatHistory = {};
|
||||
let numAdded = 0;
|
||||
chatHistory.forEach((chatHistory) => {
|
||||
if (!groupedData[chatHistory.agent_name]) {
|
||||
groupedData[chatHistory.agent_name] = [];
|
||||
}
|
||||
groupedData[chatHistory.agent_name].push(chatHistory);
|
||||
|
||||
// Add to subsetOrganizedData if less than 8
|
||||
if (numAdded < 8) {
|
||||
if (!subsetOrganizedData[chatHistory.agent_name]) {
|
||||
subsetOrganizedData[chatHistory.agent_name] = [];
|
||||
}
|
||||
subsetOrganizedData[chatHistory.agent_name].push(chatHistory);
|
||||
numAdded++;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
setSubsetOrganizedData(subsetOrganizedData);
|
||||
setOrganizedData(groupedData);
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
fetch('/api/v1/user', { method: 'GET' })
|
||||
.then(response => response.json())
|
||||
@@ -78,66 +263,79 @@ export default function SidePanel() {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${styles.panel}`}>
|
||||
return (
|
||||
<div className={`${styles.panel}`}>
|
||||
{
|
||||
enabled ?
|
||||
<div>
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<div className={`${styles.expanded}`}>
|
||||
<div className={`${styles.profile}`}>
|
||||
{ userProfile &&
|
||||
<div className={styles.profile}>
|
||||
<img
|
||||
className={styles.profile}
|
||||
src={userProfile.photo}
|
||||
alt="profile"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<p>{userProfile?.username}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button className={styles.button} onClick={() => setEnabled(false)}>
|
||||
{/* Push Close Icon */}
|
||||
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M8.70710678,12 L19.5,12 C19.7761424,12 20,12.2238576 20,12.5 C20,12.7761424 19.7761424,13 19.5,13 L8.70710678,13 L11.8535534,16.1464466 C12.0488155,16.3417088 12.0488155,16.6582912 11.8535534,16.8535534 C11.6582912,17.0488155 11.3417088,17.0488155 11.1464466,16.8535534 L7.14644661,12.8535534 C6.95118446,12.6582912 6.95118446,12.3417088 7.14644661,12.1464466 L11.1464466,8.14644661 C11.3417088,7.95118446 11.6582912,7.95118446 11.8535534,8.14644661 C12.0488155,8.34170876 12.0488155,8.65829124 11.8535534,8.85355339 L8.70710678,12 L8.70710678,12 Z M4,5.5 C4,5.22385763 4.22385763,5 4.5,5 C4.77614237,5 5,5.22385763 5,5.5 L5,19.5 C5,19.7761424 4.77614237,20 4.5,20 C4.22385763,20 4,19.7761424 4,19.5 L4,5.5 Z"></path> </g></svg>
|
||||
</button>
|
||||
<h3>Recent Conversations</h3>
|
||||
</div>
|
||||
<div className={styles.sessionsList}>
|
||||
{dataToShow && dataToShow.map((chatHistory) => (
|
||||
<ChatSession key={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id} slug={chatHistory.slug} />
|
||||
))}
|
||||
</div>
|
||||
<ScrollArea className="h-[40vh] w-[14rem]">
|
||||
<div className={styles.sessionsList}>
|
||||
{subsetOrganizedData && Object.keys(subsetOrganizedData).map((agentName) => (
|
||||
<div key={agentName} className={`my-4`}>
|
||||
<h3 className={`grid grid-flow-col auto-cols-max gap-2 my-4 font-bold text-sm`}>
|
||||
<img src={subsetOrganizedData[agentName][0].agent_avatar} alt={agentName} width={24} height={24} />
|
||||
{agentName}
|
||||
</h3>
|
||||
{subsetOrganizedData[agentName].map((chatHistory) => (
|
||||
<ChatSession
|
||||
compressed={true}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={chatHistory.conversation_id}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{
|
||||
(data && data.length > 5) && (
|
||||
(isExpanded) ?
|
||||
<ChatSessionsModal data={data} setIsExpanded={setIsExpanded} />
|
||||
:
|
||||
<button className={styles.showMoreButton} onClick={() => {
|
||||
setIsExpanded(true);
|
||||
}}>
|
||||
Show All
|
||||
</button>
|
||||
<ChatSessionsModal data={organizedData} />
|
||||
)
|
||||
}
|
||||
{userProfile &&
|
||||
<div className={styles.profile}>
|
||||
<Avatar>
|
||||
<AvatarImage src={userProfile.photo} alt="user profile" />
|
||||
<AvatarFallback>
|
||||
{userProfile.username[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className={styles.profileDetails}>
|
||||
<p>{userProfile?.username}</p>
|
||||
{/* Connected Indicator */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className={`inline-flex h-4 w-4 rounded-full opacity-75 ${props.webSocketConnected ? 'bg-green-500' : 'bg-rose-500'}`}></div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{props.webSocketConnected ? "Connected" : "Disconnected"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
:
|
||||
<div>
|
||||
<div className={`${styles.collapsed}`}>
|
||||
{ userProfile &&
|
||||
<div className={`${styles.profile}`}>
|
||||
<img
|
||||
className={styles.profile}
|
||||
src={userProfile.photo}
|
||||
alt="profile"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{userProfile &&
|
||||
<div className={styles.profile}>
|
||||
<Avatar className="max-w-6 max-h-6 rounded-full">
|
||||
<AvatarImage src={userProfile.photo} alt="user profile" />
|
||||
<AvatarFallback>
|
||||
{userProfile.username[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
}
|
||||
<button className={styles.button} onClick={() => setEnabled(true)}>
|
||||
{/* Pull Open Icon */}
|
||||
<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M15.2928932,12 L12.1464466,8.85355339 C11.9511845,8.65829124 11.9511845,8.34170876 12.1464466,8.14644661 C12.3417088,7.95118446 12.6582912,7.95118446 12.8535534,8.14644661 L16.8535534,12.1464466 C17.0488155,12.3417088 17.0488155,12.6582912 16.8535534,12.8535534 L12.8535534,16.8535534 C12.6582912,17.0488155 12.3417088,17.0488155 12.1464466,16.8535534 C11.9511845,16.6582912 11.9511845,16.3417088 12.1464466,16.1464466 L15.2928932,13 L4.5,13 C4.22385763,13 4,12.7761424 4,12.5 C4,12.2238576 4.22385763,12 4.5,12 L15.2928932,12 Z M19,5.5 C19,5.22385763 19.2238576,5 19.5,5 C19.7761424,5 20,5.22385763 20,5.5 L20,19.5 C20,19.7761424 19.7761424,20 19.5,20 C19.2238576,20 19,19.7761424 19,19.5 L19,5.5 Z"></path> </g></svg>
|
||||
@@ -146,6 +344,6 @@ export default function SidePanel() {
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,27 @@ 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, 350px) 1fr;
|
||||
}
|
||||
|
||||
div.compressed {
|
||||
grid-template-columns: minmax(auto, 12rem) 1fr 1fr;
|
||||
}
|
||||
|
||||
div.sessionHover {
|
||||
background-color: var(--secondary-accent);
|
||||
}
|
||||
|
||||
div.session:hover {
|
||||
background-color: var(--secondary-accent);
|
||||
}
|
||||
|
||||
div.session a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button.button {
|
||||
@@ -17,9 +35,6 @@ button.button {
|
||||
}
|
||||
|
||||
button.showMoreButton {
|
||||
background: var(--intense-green);
|
||||
border: none;
|
||||
color: var(--frosted-background-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -28,9 +43,8 @@ div.panel {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--calm-blue);
|
||||
color: var(--main-text-color);
|
||||
background-color: hsla(var(--primary-foreground));
|
||||
background-color: var(--secondary-background-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
max-width: auto;
|
||||
@@ -47,14 +61,12 @@ div.collapsed {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
div.session:hover {
|
||||
background-color: var(--calmer-blue);
|
||||
}
|
||||
|
||||
p.session {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
div.header {
|
||||
@@ -62,10 +74,18 @@ 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;
|
||||
align-self: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
div.panelWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,3 +116,13 @@ div.modalSessionsList div.session {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
div.panel {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
div.singleReference {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user