mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-18 17:13:02 +02:00
update- reorg
This commit is contained in:
@ -826,7 +826,7 @@ export function ChatInputBar({
|
|||||||
chatState == "toolBuilding" ||
|
chatState == "toolBuilding" ||
|
||||||
chatState == "loading" ? (
|
chatState == "loading" ? (
|
||||||
<StopGeneratingIcon
|
<StopGeneratingIcon
|
||||||
size={10}
|
size={8}
|
||||||
className="text-emphasis m-auto text-white flex-none"
|
className="text-emphasis m-auto text-white flex-none"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -76,9 +76,9 @@ import {
|
|||||||
StreamingPhase,
|
StreamingPhase,
|
||||||
StreamingPhaseText,
|
StreamingPhaseText,
|
||||||
useStreamingMessages,
|
useStreamingMessages,
|
||||||
useOrderedPhases,
|
|
||||||
} from "./StreamingMessages";
|
} from "./StreamingMessages";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import RefinemenetBadge from "../refinmentBadge";
|
||||||
|
|
||||||
export const AgenticMessage = ({
|
export const AgenticMessage = ({
|
||||||
secondLevelAssistantMessage,
|
secondLevelAssistantMessage,
|
||||||
@ -374,22 +374,6 @@ export const AgenticMessage = ({
|
|||||||
);
|
);
|
||||||
}, [streamedContent, markdownComponents]);
|
}, [streamedContent, markdownComponents]);
|
||||||
|
|
||||||
const currentState = secondLevelSubquestions?.[0]
|
|
||||||
? secondLevelSubquestions[0].answer
|
|
||||||
? secondLevelSubquestions[0].is_complete
|
|
||||||
? StreamingPhase.COMPLETE
|
|
||||||
: StreamingPhase.ANSWER
|
|
||||||
: secondLevelSubquestions[0].context_docs
|
|
||||||
? StreamingPhase.CONTEXT_DOCS
|
|
||||||
: secondLevelSubquestions[0].sub_queries
|
|
||||||
? StreamingPhase.SUB_QUERIES
|
|
||||||
: secondLevelSubquestions[0].question
|
|
||||||
? StreamingPhase.WAITING
|
|
||||||
: StreamingPhase.WAITING
|
|
||||||
: StreamingPhase.WAITING;
|
|
||||||
|
|
||||||
const message = useOrderedPhases(currentState);
|
|
||||||
|
|
||||||
const includeMessageSwitcher =
|
const includeMessageSwitcher =
|
||||||
currentMessageInd !== undefined &&
|
currentMessageInd !== undefined &&
|
||||||
onMessageSelection &&
|
onMessageSelection &&
|
||||||
@ -495,45 +479,16 @@ export const AgenticMessage = ({
|
|||||||
Answer
|
Answer
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{secondLevelSubquestions &&
|
{true ? (
|
||||||
secondLevelSubquestions.length > 0 &&
|
<RefinemenetBadge
|
||||||
secondLevelGenerating ? (
|
secondLevelSubquestions={secondLevelSubquestions}
|
||||||
<Popover>
|
toggleInitialAnswerVieinwg={() => {
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div className="flex loading-text items-center gap-x-2 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
|
|
||||||
Refining answer...
|
|
||||||
<FiChevronRight
|
|
||||||
className="inline-block text-text-darker"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-80 p-4 bg-white shadow-lg rounded-md">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-lg leading-none font-semibold text-gray-800">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
The answer is being refined based on
|
|
||||||
additional context and analysis.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
setIsViewingInitialAnswer(
|
setIsViewingInitialAnswer(
|
||||||
!isViewingInitialAnswer
|
!isViewingInitialAnswer
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
size="sm"
|
isViewingInitialAnswer={isViewingInitialAnswer}
|
||||||
className="w-full"
|
/>
|
||||||
>
|
|
||||||
{isViewingInitialAnswer
|
|
||||||
? "See Live Updates"
|
|
||||||
: "Hide Live Updates"}
|
|
||||||
<FiGlobe className="inline-block mr-2" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
) : (
|
) : (
|
||||||
secondLevelAssistantMessage && (
|
secondLevelAssistantMessage && (
|
||||||
<Badge
|
<Badge
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||||
import { ResultIcon } from "@/components/chat_search/sources/SourceCard";
|
import {
|
||||||
|
ResultIcon,
|
||||||
|
SeeMoreBlock,
|
||||||
|
} from "@/components/chat_search/sources/SourceCard";
|
||||||
import { openDocument } from "@/lib/search/utils";
|
import { openDocument } from "@/lib/search/utils";
|
||||||
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
|
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
|
||||||
|
import { ValidSources } from "@/lib/types";
|
||||||
|
|
||||||
interface SourcesDisplayProps {
|
interface SourcesDisplayProps {
|
||||||
documents: OnyxDocument[];
|
documents: OnyxDocument[];
|
||||||
@ -103,19 +107,16 @@ export const SourcesDisplay: React.FC<SourcesDisplayProps> = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{hasMoreDocuments && (
|
{hasMoreDocuments && (
|
||||||
<button
|
<SeeMoreBlock
|
||||||
onClick={toggleDocumentSelection}
|
toggled={false}
|
||||||
className={`max-w-[260px] h-[80px] p-3 bg-[#f1eee8] hover:bg-[#ebe7de] cursor-pointer rounded-lg flex flex-col items-start justify-between transition-opacity duration-300 ${
|
toggleDocumentSelection={toggleDocumentSelection}
|
||||||
animateEntrance ? "opacity-100" : "opacity-100"
|
uniqueSources={
|
||||||
}`}
|
Array.from(
|
||||||
>
|
new Set(documents.map((doc) => doc.source_type))
|
||||||
<div className="flex items-center gap-1">
|
) as ValidSources[]
|
||||||
{documents.slice(3, 6).map((doc, index) => (
|
}
|
||||||
<ResultIcon key={index} doc={doc} size={14} />
|
webSourceDomains={documents.map((doc) => doc.link)}
|
||||||
))}
|
/>
|
||||||
</div>
|
|
||||||
<div className="text-[#4a4a4a] text-xs font-medium">Show All</div>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,86 +31,12 @@ interface SubQuestionProgress {
|
|||||||
answerCharIndex: number;
|
answerCharIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PHASES_ORDER: StreamingPhase[] = [
|
|
||||||
StreamingPhase.WAITING,
|
|
||||||
StreamingPhase.SUB_QUERIES,
|
|
||||||
StreamingPhase.CONTEXT_DOCS,
|
|
||||||
StreamingPhase.ANSWER,
|
|
||||||
StreamingPhase.COMPLETE,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const PHASE_MIN_MS = 800; // Minimum phase duration in ms
|
export const PHASE_MIN_MS = 800; // Minimum phase duration in ms
|
||||||
|
|
||||||
function canTransition(p: SubQuestionProgress) {
|
function canTransition(p: SubQuestionProgress) {
|
||||||
return Date.now() - p.phaseStartTime >= PHASE_MIN_MS;
|
return Date.now() - p.phaseStartTime >= PHASE_MIN_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOrderedPhases(externalPhase: StreamingPhase) {
|
|
||||||
const [phaseQueue, setPhaseQueue] = useState<StreamingPhase[]>([]);
|
|
||||||
const [displayedPhase, setDisplayedPhase] = useState<StreamingPhase>(
|
|
||||||
StreamingPhase.WAITING
|
|
||||||
);
|
|
||||||
const lastDisplayTimestampRef = useRef<number>(Date.now());
|
|
||||||
|
|
||||||
const getPhaseIndex = (phase: StreamingPhase) => {
|
|
||||||
return PHASES_ORDER.indexOf(phase);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPhaseQueue((prevQueue) => {
|
|
||||||
const lastQueuedPhase =
|
|
||||||
prevQueue.length > 0 ? prevQueue[prevQueue.length - 1] : displayedPhase;
|
|
||||||
|
|
||||||
const lastQueuedIndex = getPhaseIndex(lastQueuedPhase);
|
|
||||||
const externalIndex = getPhaseIndex(externalPhase);
|
|
||||||
|
|
||||||
if (externalIndex <= lastQueuedIndex) {
|
|
||||||
return prevQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingPhases: StreamingPhase[] = [];
|
|
||||||
for (let i = lastQueuedIndex + 1; i <= externalIndex; i++) {
|
|
||||||
missingPhases.push(PHASES_ORDER[i]);
|
|
||||||
}
|
|
||||||
return [...prevQueue, ...missingPhases];
|
|
||||||
});
|
|
||||||
}, [externalPhase, displayedPhase]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (phaseQueue.length === 0) return;
|
|
||||||
let rafId: number;
|
|
||||||
|
|
||||||
const processQueue = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const elapsed = now - lastDisplayTimestampRef.current;
|
|
||||||
|
|
||||||
// Keep this at 1000ms from the original example (unchanged),
|
|
||||||
// but you can adjust if you want a different visible time in *this* component.
|
|
||||||
if (elapsed >= 1000) {
|
|
||||||
setPhaseQueue((prevQueue) => {
|
|
||||||
if (prevQueue.length > 0) {
|
|
||||||
const [next, ...rest] = prevQueue;
|
|
||||||
setDisplayedPhase(next);
|
|
||||||
lastDisplayTimestampRef.current = Date.now();
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
return prevQueue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
rafId = requestAnimationFrame(processQueue);
|
|
||||||
};
|
|
||||||
|
|
||||||
rafId = requestAnimationFrame(processQueue);
|
|
||||||
return () => {
|
|
||||||
if (rafId) {
|
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [phaseQueue]);
|
|
||||||
|
|
||||||
return StreamingPhaseText[displayedPhase];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DOC_DELAY_MS = 100;
|
const DOC_DELAY_MS = 100;
|
||||||
|
|
||||||
export const useStreamingMessages = (
|
export const useStreamingMessages = (
|
||||||
|
@ -506,9 +506,12 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (documents && documents.length > 0) {
|
if (documents && documents.length > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
|
() => {
|
||||||
setShownDocuments(documents);
|
setShownDocuments(documents);
|
||||||
}, 1500);
|
},
|
||||||
|
overallAnswerGenerating ? 1500 : 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [documents]);
|
}, [documents]);
|
||||||
|
|
||||||
@ -639,7 +642,7 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
|||||||
documents={documents}
|
documents={documents}
|
||||||
isLast={
|
isLast={
|
||||||
!showSummarizing &&
|
!showSummarizing &&
|
||||||
memoizedSubQuestions.length > index + 1 &&
|
memoizedSubQuestions.length == index + 1 &&
|
||||||
!(
|
!(
|
||||||
showSecondLevel &&
|
showSecondLevel &&
|
||||||
memoizedSecondLevelQuestions &&
|
memoizedSecondLevelQuestions &&
|
||||||
@ -689,7 +692,7 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
|||||||
documents={documents}
|
documents={documents}
|
||||||
isLast={
|
isLast={
|
||||||
!showSummarizing &&
|
!showSummarizing &&
|
||||||
memoizedSecondLevelQuestions.length > index + 1
|
memoizedSecondLevelQuestions.length == index + 1
|
||||||
}
|
}
|
||||||
isFirst={false}
|
isFirst={false}
|
||||||
setPresentingDocument={setPresentingDocument}
|
setPresentingDocument={setPresentingDocument}
|
||||||
|
152
web/src/app/chat/refinmentBadge.tsx
Normal file
152
web/src/app/chat/refinmentBadge.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipContent,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { FiChevronRight, FiGlobe } from "react-icons/fi";
|
||||||
|
import {
|
||||||
|
StreamingPhase,
|
||||||
|
StreamingPhaseText,
|
||||||
|
} from "./message/StreamingMessages";
|
||||||
|
import { SubQuestionDetail } from "./interfaces";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const PHASES_ORDER: StreamingPhase[] = [
|
||||||
|
StreamingPhase.WAITING,
|
||||||
|
StreamingPhase.SUB_QUERIES,
|
||||||
|
StreamingPhase.CONTEXT_DOCS,
|
||||||
|
StreamingPhase.ANSWER,
|
||||||
|
StreamingPhase.COMPLETE,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useOrderedPhases(externalPhase: StreamingPhase) {
|
||||||
|
const [phaseQueue, setPhaseQueue] = useState<StreamingPhase[]>([]);
|
||||||
|
const [displayedPhase, setDisplayedPhase] = useState<StreamingPhase>(
|
||||||
|
StreamingPhase.WAITING
|
||||||
|
);
|
||||||
|
const lastDisplayTimestampRef = useRef<number>(Date.now());
|
||||||
|
|
||||||
|
const getPhaseIndex = (phase: StreamingPhase) => {
|
||||||
|
return PHASES_ORDER.indexOf(phase);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPhaseQueue((prevQueue) => {
|
||||||
|
const lastQueuedPhase =
|
||||||
|
prevQueue.length > 0 ? prevQueue[prevQueue.length - 1] : displayedPhase;
|
||||||
|
|
||||||
|
const lastQueuedIndex = getPhaseIndex(lastQueuedPhase);
|
||||||
|
const externalIndex = getPhaseIndex(externalPhase);
|
||||||
|
|
||||||
|
if (externalIndex <= lastQueuedIndex) {
|
||||||
|
return prevQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingPhases: StreamingPhase[] = [];
|
||||||
|
for (let i = lastQueuedIndex + 1; i <= externalIndex; i++) {
|
||||||
|
missingPhases.push(PHASES_ORDER[i]);
|
||||||
|
}
|
||||||
|
return [...prevQueue, ...missingPhases];
|
||||||
|
});
|
||||||
|
}, [externalPhase, displayedPhase]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phaseQueue.length === 0) return;
|
||||||
|
let rafId: number;
|
||||||
|
|
||||||
|
const processQueue = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - lastDisplayTimestampRef.current;
|
||||||
|
|
||||||
|
// Keep this at 1000ms from the original example (unchanged),
|
||||||
|
// but you can adjust if you want a different visible time in *this* component.
|
||||||
|
if (elapsed >= 1000) {
|
||||||
|
setPhaseQueue((prevQueue) => {
|
||||||
|
if (prevQueue.length > 0) {
|
||||||
|
const [next, ...rest] = prevQueue;
|
||||||
|
setDisplayedPhase(next);
|
||||||
|
lastDisplayTimestampRef.current = Date.now();
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return prevQueue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(processQueue);
|
||||||
|
};
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(processQueue);
|
||||||
|
return () => {
|
||||||
|
if (rafId) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [phaseQueue]);
|
||||||
|
|
||||||
|
return StreamingPhaseText[displayedPhase];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RefinemenetBadge({
|
||||||
|
secondLevelSubquestions,
|
||||||
|
toggleInitialAnswerVieinwg,
|
||||||
|
isViewingInitialAnswer,
|
||||||
|
}: {
|
||||||
|
secondLevelSubquestions?: SubQuestionDetail[] | null;
|
||||||
|
toggleInitialAnswerVieinwg: () => void;
|
||||||
|
isViewingInitialAnswer: boolean;
|
||||||
|
}) {
|
||||||
|
const currentState = secondLevelSubquestions?.[0]
|
||||||
|
? secondLevelSubquestions[0].answer
|
||||||
|
? secondLevelSubquestions[0].is_complete
|
||||||
|
? StreamingPhase.COMPLETE
|
||||||
|
: StreamingPhase.ANSWER
|
||||||
|
: secondLevelSubquestions[0].context_docs
|
||||||
|
? StreamingPhase.CONTEXT_DOCS
|
||||||
|
: secondLevelSubquestions[0].sub_queries
|
||||||
|
? StreamingPhase.SUB_QUERIES
|
||||||
|
: secondLevelSubquestions[0].question
|
||||||
|
? StreamingPhase.WAITING
|
||||||
|
: StreamingPhase.WAITING
|
||||||
|
: StreamingPhase.WAITING;
|
||||||
|
|
||||||
|
const message = useOrderedPhases(currentState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex loading-text items-center gap-x-2 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
|
||||||
|
Refining answer...
|
||||||
|
<FiChevronRight
|
||||||
|
className="inline-block text-text-darker"
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="w-80 p-4 bg-white shadow-lg rounded-md">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-lg leading-none font-semibold text-gray-800">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
The answer is being refined based on additional context and
|
||||||
|
analysis.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleInitialAnswerVieinwg()}
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isViewingInitialAnswer
|
||||||
|
? "See Live Updates"
|
||||||
|
: "Hide Live Updates"}
|
||||||
|
<FiGlobe className="inline-block mr-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -71,37 +71,29 @@ export function SeeMoreBlock({
|
|||||||
);
|
);
|
||||||
const numOfWebSourcesToDisplay = 3 - filteredUniqueSources.length;
|
const numOfWebSourcesToDisplay = 3 - filteredUniqueSources.length;
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
onClick={toggleDocumentSelection}
|
onClick={toggleDocumentSelection}
|
||||||
className={`
|
className={`max-w-[260px] min-w-[100px] h-[80px] p-3 bg-[#f1eee8] hover:bg-[#ebe7de] cursor-pointer rounded-lg flex flex-col items-start justify-between transition-opacity duration-300`}
|
||||||
cursor-pointer rounded-lg flex-none transition-all duration-500 hover:bg-background-dark/80 bg-background-dark/60 px-3 py-2
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div className="flex gap-y-2 flex-col items-start text-sm">
|
<div className="flex items-center gap-1">
|
||||||
<p
|
|
||||||
onClick={toggleDocumentSelection}
|
|
||||||
className="flex-1 mr-1 font-semibold text-text-900 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{toggled ? "Hide Results" : "Show Sources"}
|
|
||||||
</p>
|
|
||||||
<div className="flex-shrink-0 flex gap-x-1 items-center">
|
|
||||||
{filteredUniqueSources.slice(0, 3).map((source, index) => (
|
{filteredUniqueSources.slice(0, 3).map((source, index) => (
|
||||||
<SourceIcon key={index} sourceType={source} iconSize={16} />
|
<SourceIcon key={index} sourceType={source} iconSize={14} />
|
||||||
))}
|
))}
|
||||||
{/* {webSourceDomains
|
{webSourceDomains
|
||||||
.slice(0, numOfWebSourcesToDisplay)
|
.slice(0, numOfWebSourcesToDisplay)
|
||||||
.map((domain, ind) => (
|
.map((domain, index) => (
|
||||||
<WebResultIcon key={ind} url={domain} />
|
<WebResultIcon key={index} url={domain} size={14} />
|
||||||
))}
|
))}
|
||||||
<WebResultIcon url={domain} />
|
|
||||||
))} */}
|
|
||||||
{uniqueSources.length > 3 && (
|
{uniqueSources.length > 3 && (
|
||||||
<span className="text-xs text-text-700 font-semibold ml-1">
|
<span className="text-xs text-[#4a4a4a] font-medium ml-1">
|
||||||
+{uniqueSources.length - 3}
|
+{uniqueSources.length - 3}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-text-darker text-xs font-semibold">
|
||||||
|
{toggled ? "Hide Results" : "Show All"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user