mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
fix: resolve UI accessibility, hydration, and semantic HTML issues (#520)
Co-authored-by: Mahesh Sanikommu <mahesh.svmr@gmail.com>
This commit is contained in:
parent
1028c33e58
commit
08267ffa21
8 changed files with 322 additions and 314 deletions
|
|
@ -1,23 +1,27 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import NextError from "next/error";
|
||||
import { useEffect } from "react";
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import NextError from "next/error"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
definition requires a `statusCode` prop. However, since the App Router
|
||||
does not expose status codes for errors, we simply pass 0 to render a
|
||||
generic error message. */}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${font.variable} antialiased overflow-x-hidden`}>
|
||||
<body
|
||||
className={`${font.variable} antialiased overflow-x-hidden`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"
|
|||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import type { z } from "zod"
|
||||
import { MemoryGraph } from "@repo/ui/memory-graph"
|
||||
import { Dialog, DialogContent } from "@repo/ui/components/dialog"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal"
|
||||
import { AddMemoryView } from "@/components/views/add-memory"
|
||||
import { useChatOpen, useProject, useGraphModal } from "@/stores"
|
||||
|
|
@ -173,6 +173,7 @@ export function GraphDialog() {
|
|||
className="w-[95vw] h-[95vh] p-0 max-w-6xl sm:max-w-6xl"
|
||||
showCloseButton={true}
|
||||
>
|
||||
<DialogTitle className="sr-only">Memory Graph</DialogTitle>
|
||||
<div className="w-full h-full">
|
||||
<MemoryGraph
|
||||
documents={allDocuments}
|
||||
|
|
|
|||
|
|
@ -1,294 +1,290 @@
|
|||
'use client';
|
||||
import { cn } from '@lib/utils';
|
||||
import {
|
||||
AnimatePresence,
|
||||
motion
|
||||
} from 'motion/react';
|
||||
"use client"
|
||||
import { cn } from "@lib/utils"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import type {
|
||||
TargetAndTransition,
|
||||
Transition,
|
||||
Variant,
|
||||
Variants,
|
||||
} from 'motion/react'
|
||||
import React from 'react';
|
||||
TargetAndTransition,
|
||||
Transition,
|
||||
Variant,
|
||||
Variants,
|
||||
} from "motion/react"
|
||||
import React from "react"
|
||||
|
||||
export type PresetType = 'blur' | 'fade-in-blur' | 'scale' | 'fade' | 'slide';
|
||||
export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide"
|
||||
|
||||
export type PerType = 'word' | 'char' | 'line';
|
||||
export type PerType = "word" | "char" | "line"
|
||||
|
||||
export type TextEffectProps = {
|
||||
children: string;
|
||||
per?: PerType;
|
||||
as?: keyof React.JSX.IntrinsicElements;
|
||||
variants?: {
|
||||
container?: Variants;
|
||||
item?: Variants;
|
||||
};
|
||||
className?: string;
|
||||
preset?: PresetType;
|
||||
delay?: number;
|
||||
speedReveal?: number;
|
||||
speedSegment?: number;
|
||||
trigger?: boolean;
|
||||
onAnimationComplete?: () => void;
|
||||
onAnimationStart?: () => void;
|
||||
segmentWrapperClassName?: string;
|
||||
containerTransition?: Transition;
|
||||
segmentTransition?: Transition;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
children: string
|
||||
per?: PerType
|
||||
as?: keyof React.JSX.IntrinsicElements
|
||||
variants?: {
|
||||
container?: Variants
|
||||
item?: Variants
|
||||
}
|
||||
className?: string
|
||||
preset?: PresetType
|
||||
delay?: number
|
||||
speedReveal?: number
|
||||
speedSegment?: number
|
||||
trigger?: boolean
|
||||
onAnimationComplete?: () => void
|
||||
onAnimationStart?: () => void
|
||||
segmentWrapperClassName?: string
|
||||
containerTransition?: Transition
|
||||
segmentTransition?: Transition
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const defaultStaggerTimes: Record<PerType, number> = {
|
||||
char: 0.03,
|
||||
word: 0.05,
|
||||
line: 0.1,
|
||||
};
|
||||
char: 0.03,
|
||||
word: 0.05,
|
||||
line: 0.1,
|
||||
}
|
||||
|
||||
const defaultContainerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
||||
},
|
||||
};
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
||||
},
|
||||
}
|
||||
|
||||
const defaultItemVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
},
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
},
|
||||
exit: { opacity: 0 },
|
||||
}
|
||||
|
||||
const presetVariants: Record<
|
||||
PresetType,
|
||||
{ container: Variants; item: Variants }
|
||||
PresetType,
|
||||
{ container: Variants; item: Variants }
|
||||
> = {
|
||||
blur: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, filter: 'blur(12px)' },
|
||||
visible: { opacity: 1, filter: 'blur(0px)' },
|
||||
exit: { opacity: 0, filter: 'blur(12px)' },
|
||||
},
|
||||
},
|
||||
'fade-in-blur': {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, y: 20, filter: 'blur(12px)' },
|
||||
visible: { opacity: 1, y: 0, filter: 'blur(0px)' },
|
||||
exit: { opacity: 0, y: 20, filter: 'blur(12px)' },
|
||||
},
|
||||
},
|
||||
scale: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, scale: 0 },
|
||||
visible: { opacity: 1, scale: 1 },
|
||||
exit: { opacity: 0, scale: 0 },
|
||||
},
|
||||
},
|
||||
fade: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
},
|
||||
},
|
||||
slide: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: 20 },
|
||||
},
|
||||
},
|
||||
};
|
||||
blur: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, filter: "blur(12px)" },
|
||||
visible: { opacity: 1, filter: "blur(0px)" },
|
||||
exit: { opacity: 0, filter: "blur(12px)" },
|
||||
},
|
||||
},
|
||||
"fade-in-blur": {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, y: 20, filter: "blur(12px)" },
|
||||
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||
exit: { opacity: 0, y: 20, filter: "blur(12px)" },
|
||||
},
|
||||
},
|
||||
scale: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, scale: 0 },
|
||||
visible: { opacity: 1, scale: 1 },
|
||||
exit: { opacity: 0, scale: 0 },
|
||||
},
|
||||
},
|
||||
fade: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
},
|
||||
},
|
||||
slide: {
|
||||
container: defaultContainerVariants,
|
||||
item: {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: 20 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const AnimationComponent: React.FC<{
|
||||
segment: string;
|
||||
variants: Variants;
|
||||
per: 'line' | 'word' | 'char';
|
||||
segmentWrapperClassName?: string;
|
||||
segment: string
|
||||
variants: Variants
|
||||
per: "line" | "word" | "char"
|
||||
segmentWrapperClassName?: string
|
||||
}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {
|
||||
const content =
|
||||
per === 'line' ? (
|
||||
<motion.span variants={variants} className='block'>
|
||||
{segment}
|
||||
</motion.span>
|
||||
) : per === 'word' ? (
|
||||
<motion.span
|
||||
aria-hidden='true'
|
||||
variants={variants}
|
||||
className='inline-block whitespace-pre'
|
||||
>
|
||||
{segment}
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span className='inline-block whitespace-pre'>
|
||||
{segment.split('').map((char, charIndex) => (
|
||||
<motion.span
|
||||
key={`char-${charIndex}`}
|
||||
aria-hidden='true'
|
||||
variants={variants}
|
||||
className='inline-block whitespace-pre'
|
||||
>
|
||||
{char}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.span>
|
||||
);
|
||||
const content =
|
||||
per === "line" ? (
|
||||
<motion.span variants={variants} className="block">
|
||||
{segment}
|
||||
</motion.span>
|
||||
) : per === "word" ? (
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
variants={variants}
|
||||
className="inline-block whitespace-pre"
|
||||
>
|
||||
{segment}
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span className="inline-block whitespace-pre">
|
||||
{segment.split("").map((char, charIndex) => (
|
||||
<motion.span
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Character position is stable within animation
|
||||
key={`char-${charIndex}`}
|
||||
aria-hidden="true"
|
||||
variants={variants}
|
||||
className="inline-block whitespace-pre"
|
||||
>
|
||||
{char}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.span>
|
||||
)
|
||||
|
||||
if (!segmentWrapperClassName) {
|
||||
return content;
|
||||
}
|
||||
if (!segmentWrapperClassName) {
|
||||
return content
|
||||
}
|
||||
|
||||
const defaultWrapperClassName = per === 'line' ? 'block' : 'inline-block';
|
||||
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
|
||||
|
||||
return (
|
||||
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
|
||||
{content}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
AnimationComponent.displayName = 'AnimationComponent';
|
||||
AnimationComponent.displayName = "AnimationComponent"
|
||||
|
||||
const splitText = (text: string, per: PerType) => {
|
||||
if (per === 'line') return text.split('\n');
|
||||
return text.split(/(\s+)/);
|
||||
};
|
||||
if (per === "line") return text.split("\n")
|
||||
return text.split(/(\s+)/)
|
||||
}
|
||||
|
||||
const hasTransition = (
|
||||
variant?: Variant
|
||||
variant?: Variant,
|
||||
): variant is TargetAndTransition & { transition?: Transition } => {
|
||||
if (!variant) return false;
|
||||
return (
|
||||
typeof variant === 'object' && 'transition' in variant
|
||||
);
|
||||
};
|
||||
if (!variant) return false
|
||||
return typeof variant === "object" && "transition" in variant
|
||||
}
|
||||
|
||||
const createVariantsWithTransition = (
|
||||
baseVariants: Variants,
|
||||
transition?: Transition & { exit?: Transition }
|
||||
baseVariants: Variants,
|
||||
transition?: Transition & { exit?: Transition },
|
||||
): Variants => {
|
||||
if (!transition) return baseVariants;
|
||||
if (!transition) return baseVariants
|
||||
|
||||
const { exit: _, ...mainTransition } = transition;
|
||||
const { exit: _, ...mainTransition } = transition
|
||||
|
||||
return {
|
||||
...baseVariants,
|
||||
visible: {
|
||||
...baseVariants.visible,
|
||||
transition: {
|
||||
...(hasTransition(baseVariants.visible)
|
||||
? baseVariants.visible.transition
|
||||
: {}),
|
||||
...mainTransition,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
...baseVariants.exit,
|
||||
transition: {
|
||||
...(hasTransition(baseVariants.exit)
|
||||
? baseVariants.exit.transition
|
||||
: {}),
|
||||
...mainTransition,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
return {
|
||||
...baseVariants,
|
||||
visible: {
|
||||
...baseVariants.visible,
|
||||
transition: {
|
||||
...(hasTransition(baseVariants.visible)
|
||||
? baseVariants.visible.transition
|
||||
: {}),
|
||||
...mainTransition,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
...baseVariants.exit,
|
||||
transition: {
|
||||
...(hasTransition(baseVariants.exit)
|
||||
? baseVariants.exit.transition
|
||||
: {}),
|
||||
...mainTransition,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function TextEffect({
|
||||
children,
|
||||
per = 'word',
|
||||
as = 'p',
|
||||
variants,
|
||||
className,
|
||||
preset = 'fade',
|
||||
delay = 0,
|
||||
speedReveal = 1,
|
||||
speedSegment = 1,
|
||||
trigger = true,
|
||||
onAnimationComplete,
|
||||
onAnimationStart,
|
||||
segmentWrapperClassName,
|
||||
containerTransition,
|
||||
segmentTransition,
|
||||
style,
|
||||
children,
|
||||
per = "word",
|
||||
as = "p",
|
||||
variants,
|
||||
className,
|
||||
preset = "fade",
|
||||
delay = 0,
|
||||
speedReveal = 1,
|
||||
speedSegment = 1,
|
||||
trigger = true,
|
||||
onAnimationComplete,
|
||||
onAnimationStart,
|
||||
segmentWrapperClassName,
|
||||
containerTransition,
|
||||
segmentTransition,
|
||||
style,
|
||||
}: TextEffectProps) {
|
||||
const segments = splitText(children, per);
|
||||
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div;
|
||||
const segments = splitText(children, per)
|
||||
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
|
||||
|
||||
const baseVariants = preset
|
||||
? presetVariants[preset]
|
||||
: { container: defaultContainerVariants, item: defaultItemVariants };
|
||||
const baseVariants = preset
|
||||
? presetVariants[preset]
|
||||
: { container: defaultContainerVariants, item: defaultItemVariants }
|
||||
|
||||
const stagger = defaultStaggerTimes[per] / speedReveal;
|
||||
const stagger = defaultStaggerTimes[per] / speedReveal
|
||||
|
||||
const baseDuration = 0.3 / speedSegment;
|
||||
const baseDuration = 0.3 / speedSegment
|
||||
|
||||
const customStagger = hasTransition(variants?.container?.visible ?? {})
|
||||
? (variants?.container?.visible as TargetAndTransition).transition
|
||||
?.staggerChildren
|
||||
: undefined;
|
||||
const customStagger = hasTransition(variants?.container?.visible ?? {})
|
||||
? (variants?.container?.visible as TargetAndTransition).transition
|
||||
?.staggerChildren
|
||||
: undefined
|
||||
|
||||
const customDelay = hasTransition(variants?.container?.visible ?? {})
|
||||
? (variants?.container?.visible as TargetAndTransition).transition
|
||||
?.delayChildren
|
||||
: undefined;
|
||||
const customDelay = hasTransition(variants?.container?.visible ?? {})
|
||||
? (variants?.container?.visible as TargetAndTransition).transition
|
||||
?.delayChildren
|
||||
: undefined
|
||||
|
||||
const computedVariants = {
|
||||
container: createVariantsWithTransition(
|
||||
variants?.container || baseVariants.container,
|
||||
{
|
||||
staggerChildren: customStagger ?? stagger,
|
||||
delayChildren: customDelay ?? delay,
|
||||
...containerTransition,
|
||||
exit: {
|
||||
staggerChildren: customStagger ?? stagger,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
}
|
||||
),
|
||||
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
|
||||
duration: baseDuration,
|
||||
...segmentTransition,
|
||||
}),
|
||||
};
|
||||
const computedVariants = {
|
||||
container: createVariantsWithTransition(
|
||||
variants?.container || baseVariants.container,
|
||||
{
|
||||
staggerChildren: customStagger ?? stagger,
|
||||
delayChildren: customDelay ?? delay,
|
||||
...containerTransition,
|
||||
exit: {
|
||||
staggerChildren: customStagger ?? stagger,
|
||||
staggerDirection: -1,
|
||||
},
|
||||
},
|
||||
),
|
||||
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
|
||||
duration: baseDuration,
|
||||
...segmentTransition,
|
||||
}),
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence mode='popLayout'>
|
||||
{trigger && (
|
||||
<MotionTag
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
variants={computedVariants.container}
|
||||
className={className}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
onAnimationStart={onAnimationStart}
|
||||
style={style}
|
||||
>
|
||||
{per !== 'line' ? <span className='sr-only'>{children}</span> : null}
|
||||
{segments.map((segment, index) => (
|
||||
<AnimationComponent
|
||||
key={`${per}-${index}-${segment}`}
|
||||
segment={segment}
|
||||
variants={computedVariants.item}
|
||||
per={per}
|
||||
segmentWrapperClassName={segmentWrapperClassName}
|
||||
/>
|
||||
))}
|
||||
</MotionTag>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
{trigger && (
|
||||
<MotionTag
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
variants={computedVariants.container}
|
||||
className={className}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
onAnimationStart={onAnimationStart}
|
||||
style={style}
|
||||
>
|
||||
{per !== "line" ? <span className="sr-only">{children}</span> : null}
|
||||
{segments.map((segment, index) => (
|
||||
<AnimationComponent
|
||||
key={`${per}-${index}-${segment}`}
|
||||
segment={segment}
|
||||
variants={computedVariants.item}
|
||||
per={per}
|
||||
segmentWrapperClassName={segmentWrapperClassName}
|
||||
/>
|
||||
))}
|
||||
</MotionTag>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ export function AddMemoryView({
|
|||
return { previousMemories, optimisticId: optimisticMemory.id }
|
||||
},
|
||||
// If the mutation fails, roll back to the previous value
|
||||
onError: (error, variables, context) => {
|
||||
onError: (_error, variables, context) => {
|
||||
if (context?.previousMemories) {
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
|
|
@ -551,6 +551,7 @@ export function AddMemoryView({
|
|||
className="w-[100vw] max-w-4xl sm:max-w-4xl backdrop-blur-xl border-white/10 z-[80] h-[52vh] overflow-y-auto p-4"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<DialogTitle className="sr-only">Add Memory</DialogTitle>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as typeof activeTab)}
|
||||
|
|
@ -638,9 +639,13 @@ export function AddMemoryView({
|
|||
>
|
||||
{({ state, handleChange, handleBlur }) => (
|
||||
<>
|
||||
<div className={addContentMutation.isPending ? "opacity-50" : ""}>
|
||||
<TextEditor
|
||||
containerClassName="bg-white/5 border-white/10 rounded-md"
|
||||
<div
|
||||
className={
|
||||
addContentMutation.isPending ? "opacity-50" : ""
|
||||
}
|
||||
>
|
||||
<TextEditor
|
||||
containerClassName="bg-white/5 border-white/10 rounded-md"
|
||||
disabled={addContentMutation.isPending}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
|
|
@ -843,14 +848,14 @@ export function AddMemoryView({
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="file" className="space-y-4">
|
||||
<form
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
fileUploadForm.handleSubmit()
|
||||
}}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -938,7 +943,7 @@ export function AddMemoryView({
|
|||
</fileUploadForm.Field>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4 mt-auto">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4 mt-auto">
|
||||
<div className="flex items-end gap-4">
|
||||
{/* Left side - Project Selection */}
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { cn } from "@lib/utils";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { cn } from "@lib/utils"
|
||||
import { Button } from "@ui/components/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -9,38 +9,38 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog";
|
||||
import { ScrollArea } from "@ui/components/scroll-area";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { HistoryIcon, Plus, Trash2, X } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useChatOpen, usePersistentChat, useProject } from "@/stores";
|
||||
import { ChatMessages } from "./chat-messages";
|
||||
} from "@ui/components/dialog"
|
||||
import { ScrollArea } from "@ui/components/scroll-area"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { HistoryIcon, Plus, Trash2, X } from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useChatOpen, usePersistentChat, useProject } from "@/stores"
|
||||
import { ChatMessages } from "./chat-messages"
|
||||
|
||||
export function ChatRewrite() {
|
||||
const { setIsOpen } = useChatOpen();
|
||||
const { selectedProject } = useProject();
|
||||
const { setIsOpen } = useChatOpen()
|
||||
const { selectedProject } = useProject()
|
||||
const { conversations, currentChatId, setCurrentChatId, getCurrentChat } =
|
||||
usePersistentChat();
|
||||
usePersistentChat()
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
return [...conversations].sort((a, b) =>
|
||||
a.lastUpdated < b.lastUpdated ? 1 : -1,
|
||||
);
|
||||
}, [conversations]);
|
||||
)
|
||||
}, [conversations])
|
||||
|
||||
function handleNewChat() {
|
||||
analytics.newChatStarted();
|
||||
const newId = crypto.randomUUID();
|
||||
setCurrentChatId(newId);
|
||||
setIsDialogOpen(false);
|
||||
analytics.newChatStarted()
|
||||
const newId = crypto.randomUUID()
|
||||
setCurrentChatId(newId)
|
||||
setIsDialogOpen(false)
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoString: string): string {
|
||||
return formatDistanceToNow(new Date(isoString), { addSuffix: true });
|
||||
return formatDistanceToNow(new Date(isoString), { addSuffix: true })
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -74,18 +74,17 @@ export function ChatRewrite() {
|
|||
<ScrollArea className="max-h-96">
|
||||
<div className="flex flex-col gap-1">
|
||||
{sorted.map((c) => {
|
||||
const isActive = c.id === currentChatId;
|
||||
const isActive = c.id === currentChatId
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={c.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setCurrentChatId(c.id);
|
||||
setIsDialogOpen(false);
|
||||
setCurrentChatId(c.id)
|
||||
setIsDialogOpen(false)
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 outline-none",
|
||||
"flex items-center justify-between rounded-md px-3 py-2 outline-none w-full text-left",
|
||||
"transition-colors",
|
||||
isActive ? "bg-primary/10" : "hover:bg-muted",
|
||||
)}
|
||||
|
|
@ -111,15 +110,15 @@ export function ChatRewrite() {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
analytics.chatDeleted();
|
||||
e.stopPropagation()
|
||||
analytics.chatDeleted()
|
||||
}}
|
||||
aria-label="Delete conversation"
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{sorted.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground px-3 py-2">
|
||||
|
|
@ -152,5 +151,5 @@ export function ChatRewrite() {
|
|||
</div>
|
||||
<ChatMessages />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
|
||||
.sm-tweet-theme .react-tweet-theme {
|
||||
--tweet-container-margin: 0px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { NextResponse } from "next/server"
|
|||
export default async function proxy(request: Request) {
|
||||
console.debug("[PROXY] === PROXY START ===")
|
||||
const url = new URL(request.url)
|
||||
|
||||
console.debug("[PROXY] Path:", url.pathname)
|
||||
console.debug("[PROXY] Method:", request.method)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue