feat: monorepo

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-04-07 23:47:06 -07:00
parent fe39077849
commit a1474ca49e
144 changed files with 43821 additions and 1 deletions

View file

@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { getConnectorIcon } from './ConnectorComponents';
import { Source } from './types';
type CitationProps = {
citationId: number;
citationText: string;
position: number;
source: Source | null;
};
/**
* Citation component to handle individual citations
*/
export const Citation = ({ citationId, citationText, position, source }: CitationProps) => {
const [open, setOpen] = useState(false);
const citationKey = `citation-${citationId}-${position}`;
if (!source) return <>{citationText}</>;
return (
<span key={citationKey} className="relative inline-flex items-center">
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<sup>
<span
className="inline-flex items-center justify-center text-primary cursor-pointer bg-primary/10 hover:bg-primary/15 w-4 h-4 rounded-full text-[10px] font-medium ml-0.5 transition-colors border border-primary/20 shadow-sm"
>
{citationId}
</span>
</sup>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-80 p-0">
<Card className="border-0 shadow-none">
<div className="p-3 flex items-start gap-3">
<div className="flex-shrink-0 w-7 h-7 flex items-center justify-center bg-muted rounded-full">
{getConnectorIcon(source.connectorType || '')}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-medium text-sm text-card-foreground">{source.title}</h3>
</div>
<p className="text-sm text-muted-foreground mt-0.5">{source.description}</p>
<div className="mt-2 flex items-center text-xs text-muted-foreground">
<span className="truncate max-w-[200px]">{source.url}</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 rounded-full"
onClick={() => window.open(source.url, '_blank')}
title="Open in new tab"
>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</div>
</Card>
</DropdownMenuContent>
</DropdownMenu>
</span>
);
};
/**
* Function to render text with citations
*/
export const renderTextWithCitations = (text: string, getCitationSource: (id: number) => Source | null) => {
// Regular expression to find citation patterns like [1], [2], etc.
const citationRegex = /\[(\d+)\]/g;
const parts = [];
let lastIndex = 0;
let match;
let position = 0;
while ((match = citationRegex.exec(text)) !== null) {
// Add text before the citation
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
// Add the citation component
const citationId = parseInt(match[1], 10);
parts.push(
<Citation
key={`citation-${citationId}-${position}`}
citationId={citationId}
citationText={match[0]}
position={position}
source={getCitationSource(citationId)}
/>
);
lastIndex = match.index + match[0].length;
position++;
}
// Add any remaining text after the last citation
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts;
};

View file

@ -0,0 +1,162 @@
import React from 'react';
import {
ChevronDown,
Plus,
Search,
Globe,
BookOpen,
Sparkles,
Microscope,
Telescope,
File,
Link,
Slack,
Webhook
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Connector, ResearchMode } from './types';
// Helper function to get connector icon
export const getConnectorIcon = (connectorType: string) => {
const iconProps = { className: "h-4 w-4" };
switch(connectorType) {
case 'CRAWLED_URL':
return <Globe {...iconProps} />;
case 'FILE':
return <File {...iconProps} />;
case 'EXTENSION':
return <Webhook {...iconProps} />;
case 'SERPER_API':
case 'TAVILY_API':
return <Link {...iconProps} />;
case 'SLACK_CONNECTOR':
return <Slack {...iconProps} />;
case 'NOTION_CONNECTOR':
return <BookOpen {...iconProps} />;
case 'DEEP':
return <Sparkles {...iconProps} />;
case 'DEEPER':
return <Microscope {...iconProps} />;
case 'DEEPEST':
return <Telescope {...iconProps} />;
default:
return <Search {...iconProps} />;
}
};
export const researcherOptions: { value: ResearchMode; label: string; icon: React.ReactNode }[] = [
{
value: 'GENERAL',
label: 'General',
icon: getConnectorIcon('GENERAL')
},
{
value: 'DEEP',
label: 'Deep',
icon: getConnectorIcon('DEEP')
},
{
value: 'DEEPER',
label: 'Deeper',
icon: getConnectorIcon('DEEPER')
},
]
/**
* Displays a small icon for a connector type
*/
export const ConnectorIcon = ({ type, index = 0 }: { type: string; index?: number }) => (
<div
className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background"
style={{ zIndex: 10 - index }}
>
{getConnectorIcon(type)}
</div>
);
/**
* Displays a count indicator for additional connectors
*/
export const ConnectorCountBadge = ({ count }: { count: number }) => (
<div className="w-4 h-4 rounded-full flex items-center justify-center bg-primary text-primary-foreground text-[8px] font-medium border border-background z-0">
+{count}
</div>
);
type ConnectorButtonProps = {
selectedConnectors: string[];
onClick: () => void;
connectorSources: Connector[];
};
/**
* Button that displays selected connectors and opens connector selection dialog
*/
export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources }: ConnectorButtonProps) => {
const totalConnectors = connectorSources.length;
const selectedCount = selectedConnectors.length;
const progressPercentage = (selectedCount / totalConnectors) * 100;
// Get the name of a single selected connector
const getSingleConnectorName = () => {
const connector = connectorSources.find(c => c.type === selectedConnectors[0]);
return connector?.name || '';
};
// Get display text based on selection count
const getDisplayText = () => {
if (selectedCount === totalConnectors) return "All Connectors";
if (selectedCount === 1) return getSingleConnectorName();
return `${selectedCount} Connectors`;
};
// Render the empty state (no connectors selected)
const renderEmptyState = () => (
<>
<Plus className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">Select Connectors</span>
</>
);
// Render the selected connectors preview
const renderSelectedConnectors = () => (
<>
<div className="flex -space-x-1.5 mr-1">
{/* Show up to 3 connector icons */}
{selectedConnectors.slice(0, 3).map((type, index) => (
<ConnectorIcon key={type} type={type} index={index} />
))}
{/* Show count indicator if more than 3 connectors are selected */}
{selectedCount > 3 && <ConnectorCountBadge count={selectedCount - 3} />}
</div>
{/* Display text */}
<span className="font-medium">{getDisplayText()}</span>
</>
);
return (
<Button
variant="outline"
className="h-7 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group scale-90 origin-left"
onClick={onClick}
aria-label={selectedCount === 0 ? "Select Connectors" : `${selectedCount} connectors selected`}
>
{/* Progress indicator */}
<div
className="absolute bottom-0 left-0 h-1 bg-primary"
style={{
width: `${progressPercentage}%`,
transition: 'width 0.3s ease'
}}
/>
<div className="flex items-center gap-1.5 z-10 relative">
{selectedCount === 0 ? renderEmptyState() : renderSelectedConnectors()}
<ChevronDown className="h-3 w-3 ml-0.5 text-muted-foreground opacity-70" />
</div>
</Button>
);
};

View file

@ -0,0 +1,80 @@
import React, { RefObject, useEffect } from 'react';
/**
* Function to scroll to the bottom of a container
*/
export const scrollToBottom = (ref: RefObject<HTMLDivElement>) => {
ref.current?.scrollIntoView({ behavior: 'smooth' });
};
/**
* Hook to scroll to bottom when messages change
*/
export const useScrollToBottom = (ref: RefObject<HTMLDivElement>, dependencies: any[]) => {
useEffect(() => {
scrollToBottom(ref);
}, dependencies);
};
/**
* Function to check scroll position and update indicators
*/
export const updateScrollIndicators = (
tabsListRef: RefObject<HTMLDivElement>,
setCanScrollLeft: (value: boolean) => void,
setCanScrollRight: (value: boolean) => void
) => {
if (tabsListRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = tabsListRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 10); // 10px buffer
}
};
/**
* Hook to initialize scroll indicators and add resize listener
*/
export const useScrollIndicators = (
tabsListRef: RefObject<HTMLDivElement>,
setCanScrollLeft: (value: boolean) => void,
setCanScrollRight: (value: boolean) => void
) => {
const updateIndicators = () => updateScrollIndicators(tabsListRef, setCanScrollLeft, setCanScrollRight);
useEffect(() => {
updateIndicators();
// Add resize listener to update indicators when window size changes
window.addEventListener('resize', updateIndicators);
return () => window.removeEventListener('resize', updateIndicators);
}, []);
return updateIndicators;
};
/**
* Function to scroll tabs list left
*/
export const scrollTabsLeft = (
tabsListRef: RefObject<HTMLDivElement>,
updateIndicators: () => void
) => {
if (tabsListRef.current) {
tabsListRef.current.scrollBy({ left: -200, behavior: 'smooth' });
// Update indicators after scrolling
setTimeout(updateIndicators, 300);
}
};
/**
* Function to scroll tabs list right
*/
export const scrollTabsRight = (
tabsListRef: RefObject<HTMLDivElement>,
updateIndicators: () => void
) => {
if (tabsListRef.current) {
tabsListRef.current.scrollBy({ left: 200, behavior: 'smooth' });
// Update indicators after scrolling
setTimeout(updateIndicators, 300);
}
};

View file

@ -0,0 +1,38 @@
import React from 'react';
type SegmentedControlProps<T extends string> = {
value: T;
onChange: (value: T) => void;
options: Array<{
value: T;
label: string;
icon: React.ReactNode;
}>;
};
/**
* A segmented control component for selecting between different options
*/
function SegmentedControl<T extends string>({ value, onChange, options }: SegmentedControlProps<T>) {
return (
<div className="flex rounded-md border border-border overflow-hidden scale-90 origin-left">
{options.map((option) => (
<button
key={option.value}
className={`flex items-center gap-1 px-2 py-1 text-xs transition-colors ${
value === option.value
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted'
}`}
onClick={() => onChange(option.value)}
aria-pressed={value === option.value}
>
{option.icon}
<span>{option.label}</span>
</button>
))}
</div>
);
}
export default SegmentedControl;

View file

@ -0,0 +1,69 @@
import React from 'react';
import { Source, Connector } from './types';
/**
* Function to get sources for the main view
*/
export const getMainViewSources = (connector: Connector, initialSourcesDisplay: number) => {
return connector.sources?.slice(0, initialSourcesDisplay);
};
/**
* Function to get filtered sources for the dialog
*/
export const getFilteredSources = (connector: Connector, sourceFilter: string) => {
if (!sourceFilter.trim()) {
return connector.sources;
}
const filter = sourceFilter.toLowerCase().trim();
return connector.sources?.filter(source =>
source.title.toLowerCase().includes(filter) ||
source.description.toLowerCase().includes(filter)
);
};
/**
* Function to get paginated and filtered sources for the dialog
*/
export const getPaginatedDialogSources = (
connector: Connector,
sourceFilter: string,
expandedSources: boolean,
sourcesPage: number,
sourcesPerPage: number
) => {
const filteredSources = getFilteredSources(connector, sourceFilter);
if (expandedSources) {
return filteredSources;
}
return filteredSources?.slice(0, sourcesPage * sourcesPerPage);
};
/**
* Function to get the count of sources for a connector type
*/
export const getSourcesCount = (connectorSources: Connector[], connectorType: string) => {
const connector = connectorSources.find(c => c.type === connectorType);
return connector?.sources?.length || 0;
};
/**
* Function to get a citation source by ID
*/
export const getCitationSource = (
citationId: number,
connectorSources: Connector[]
): Source | null => {
for (const connector of connectorSources) {
const source = connector.sources?.find(s => s.id === citationId);
if (source) {
return {
...source,
connectorType: connector.type
};
}
}
return null;
};

View file

@ -0,0 +1,18 @@
// Connector sources
export const connectorSourcesMenu = [
{
id: 1,
name: "Crawled URL",
type: "CRAWLED_URL",
},
{
id: 2,
name: "File",
type: "FILE",
},
{
id: 3,
name: "Extension",
type: "EXTENSION",
},
];

View file

@ -0,0 +1,7 @@
// Export all components and utilities from the chat folder
export { default as SegmentedControl } from './SegmentedControl';
export * from './ConnectorComponents';
export * from './Citation';
export * from './SourceUtils';
export * from './ScrollUtils';
export * from './types';

View file

@ -0,0 +1,51 @@
/**
* Types for chat components
*/
export type Source = {
id: number;
title: string;
description: string;
url: string;
connectorType?: string;
};
export type Connector = {
id: number;
type: string;
name: string;
sources?: Source[];
};
export type StatusMessage = {
id: number;
message: string;
type: 'info' | 'success' | 'error' | 'warning';
timestamp: string;
};
export type ChatMessage = {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp?: string;
};
// Define message types to match useChat() structure
export type MessageRole = 'user' | 'assistant' | 'system' | 'data';
export interface ToolInvocation {
state: 'call' | 'result';
toolCallId: string;
toolName: string;
args: any;
result?: any;
}
export interface ToolInvocationUIPart {
type: 'tool-invocation';
toolInvocation: ToolInvocation;
}
export type ResearchMode = 'GENERAL' | 'DEEP' | 'DEEPER' | 'DEEPEST';