├── .eslintrc.json
├── app
├── favicon.ico
├── fonts
│ ├── GeistVF.woff
│ └── GeistMonoVF.woff
├── api
│ ├── logger.ts
│ └── validate-contact
│ │ └── route.ts
├── page.tsx
├── layout.tsx
└── tiptap.css
├── public
├── sound-effects
│ ├── sent.m4a
│ ├── unread.m4a
│ ├── reaction.m4a
│ └── received.m4a
├── vercel.svg
├── reactions
│ ├── circle.svg
│ ├── heart-white.svg
│ ├── heart-gray.svg
│ ├── like-gray.svg
│ ├── like-white.svg
│ ├── left-light-heart-overlay.svg
│ ├── left-dark-heart-overlay.svg
│ ├── left-dark-heart.svg
│ ├── left-light-heart.svg
│ ├── left-dark-blue-heart.svg
│ ├── left-light-blue-heart.svg
│ ├── left-pinned-light-heart.svg
│ ├── right-dark-heart-overlay.svg
│ ├── right-light-heart-overlay.svg
│ ├── right-dark-heart.svg
│ ├── right-light-heart.svg
│ ├── right-dark-blue-heart.svg
│ ├── right-light-blue-heart.svg
│ ├── right-pinned-light-heart.svg
│ ├── question-white.svg
│ ├── question-gray.svg
│ ├── dislike-white.svg
│ ├── dislike-gray.svg
│ ├── left-dark-like-overlay.svg
│ ├── left-light-like-overlay.svg
│ ├── left-dark-like.svg
│ ├── left-light-like.svg
│ ├── left-dark-blue-like.svg
│ ├── left-light-blue-like.svg
│ ├── left-pinned-light-like.svg
│ ├── right-dark-like-overlay.svg
│ ├── right-light-like-overlay.svg
│ ├── right-dark-blue-like.svg
│ ├── right-dark-like.svg
│ ├── right-light-blue-like.svg
│ ├── right-light-like.svg
│ ├── right-pinned-light-like.svg
│ ├── left-dark-question-overlay.svg
│ ├── left-light-question-overlay.svg
│ ├── left-dark-question.svg
│ ├── left-dark-blue-question.svg
│ ├── left-light-blue-question.svg
│ ├── left-light-question.svg
│ ├── left-pinned-light-question.svg
│ ├── right-dark-question-overlay.svg
│ ├── right-light-question-overlay.svg
│ ├── right-dark-question.svg
│ ├── right-light-question.svg
│ ├── right-dark-blue-question.svg
│ ├── right-light-blue-question.svg
│ ├── right-pinned-light-question.svg
│ ├── emphasize-white.svg
│ ├── emphasize-gray.svg
│ ├── left-dark-dislike-overlay.svg
│ ├── left-light-dislike-overlay.svg
│ ├── left-dark-blue-dislike.svg
│ ├── left-dark-dislike.svg
│ ├── left-light-blue-dislike.svg
│ ├── left-light-dislike.svg
│ ├── left-pinned-light-dislike.svg
│ ├── right-dark-dislike-overlay.svg
│ ├── right-light-dislike-overlay.svg
│ ├── right-dark-dislike.svg
│ ├── right-light-dislike.svg
│ ├── right-dark-blue-dislike.svg
│ ├── right-light-blue-dislike.svg
│ ├── right-pinned-light-dislike.svg
│ ├── left-dark-emphasize-overlay.svg
│ ├── left-light-emphasize-overlay.svg
│ ├── left-light-blue-emphasize.svg
│ ├── left-dark-blue-emphasize.svg
│ └── left-dark-emphasize.svg
├── typing-bubbles
│ ├── typing-dark.svg
│ ├── typing-light.svg
│ ├── typing-blue.svg
│ ├── chat-typing-light.svg
│ └── chat-typing-dark.svg
├── window.svg
├── file.svg
├── message-bubbles
│ ├── left-bubble-light.svg
│ ├── left-bubble-dark.svg
│ ├── right-bubble-light.svg
│ └── right-bubble-dark.svg
├── globe.svg
└── next.svg
├── config
└── site.ts
├── postcss.config.mjs
├── lib
├── utils.ts
├── contacts.ts
└── sound-effects.ts
├── next.config.ts
├── components
├── theme-provider.tsx
├── ui
│ ├── label.tsx
│ ├── input.tsx
│ ├── toaster.tsx
│ ├── switch.tsx
│ ├── popover.tsx
│ ├── avatar.tsx
│ └── button.tsx
├── theme-toggle.tsx
├── swipe-actions.tsx
├── search-bar.tsx
└── nav.tsx
├── components.json
├── evals
└── chat.eval.ts
├── README.md
├── .gitignore
├── tsconfig.json
├── types
└── index.ts
├── package.json
└── tailwind.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/public/sound-effects/sent.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/public/sound-effects/sent.m4a
--------------------------------------------------------------------------------
/public/sound-effects/unread.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/public/sound-effects/unread.m4a
--------------------------------------------------------------------------------
/public/sound-effects/reaction.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/public/sound-effects/reaction.m4a
--------------------------------------------------------------------------------
/public/sound-effects/received.m4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alanagoyal/messages/HEAD/public/sound-effects/received.m4a
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | export const siteConfig = {
2 | title: "alana goyal | messages",
3 | url: process.env.NEXT_PUBLIC_VERCEL_URL,
4 | };
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/logger.ts:
--------------------------------------------------------------------------------
1 | import { initLogger } from "braintrust";
2 |
3 | export const logger = initLogger({
4 | projectName: "messages",
5 | apiKey: process.env.BRAINTRUST_API_KEY,
6 | });
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | basePath: "/messages",
5 | };
6 |
7 | module.exports = nextConfig;
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import App from "@/components/app";
4 |
5 | export default function Home() {
6 | return (
7 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/public/reactions/circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/typing-bubbles/typing-dark.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/typing-bubbles/typing-light.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/public/typing-bubbles/typing-blue.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/evals/chat.eval.ts:
--------------------------------------------------------------------------------
1 | import { Eval, initDataset } from "braintrust";
2 | import { LLMClassifierFromTemplate } from "autoevals";
3 |
4 | const noRepeat = LLMClassifierFromTemplate({
5 | name: "No repetition",
6 | promptTemplate: "Is this chat conversation repetetive? (Y/N)\n\n{{output}}",
7 | choiceScores: { Y: 0, N: 1 },
8 | useCoT: true,
9 | });
10 |
11 | Eval("dialogue", {
12 | data: initDataset("dialogue", { dataset: "conversations" }),
13 | task: (input) => {
14 | return JSON.stringify(input);
15 | },
16 | scores: [noRepeat],
17 | });
18 |
--------------------------------------------------------------------------------
/public/typing-bubbles/chat-typing-light.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/public/typing-bubbles/chat-typing-dark.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [messages](https://alanagoyal.com/messages)
2 |
3 | i'm obsessed with re-creating apple products. this one is an imessage-inspired website that allows you to create autonomous group chats with ai personas.
4 |
5 | ## clone the repo
6 |
7 | `git clone https://github.com/alanagoyal/messages`
8 |
9 | ## install dependencies
10 |
11 | `npm install`
12 |
13 | ## api keys
14 |
15 | you'll need a [braintrust.dev](https://braintrust.dev) api key. add it to the `.env.local` file as `BRAINTRUST_API_KEY`
16 |
17 | ## run the app
18 |
19 | `npm run dev`
20 |
21 | ## deploy
22 |
23 | deployed to [vercel.com](https://vercel.com)
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 |
32 | # env files (can opt-in for committing if needed)
33 | .env*
34 |
35 | # vercel
36 | .vercel
37 |
38 | # typescript
39 | *.tsbuildinfo
40 | next-env.d.ts
41 |
--------------------------------------------------------------------------------
/public/message-bubbles/left-bubble-light.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/message-bubbles/left-bubble-dark.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/public/message-bubbles/right-bubble-light.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/public/message-bubbles/right-bubble-dark.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/lib/contacts.ts:
--------------------------------------------------------------------------------
1 | import { InitialContact } from "@/data/initial-contacts";
2 |
3 | const CONTACTS_KEY = "user_contacts";
4 |
5 | export function getUserContacts(): InitialContact[] {
6 | if (typeof window === "undefined") return [];
7 | const contacts = localStorage.getItem(CONTACTS_KEY);
8 | return contacts ? JSON.parse(contacts) : [];
9 | }
10 |
11 | export function addUserContact(name: string): InitialContact[] {
12 | const contacts = getUserContacts();
13 | const newContact: InitialContact = {
14 | name,
15 | title: "Custom Contact"
16 | };
17 |
18 | // Check if contact already exists
19 | if (!contacts.some(contact => contact.name.toLowerCase() === name.toLowerCase())) {
20 | contacts.push(newContact);
21 | localStorage.setItem(CONTACTS_KEY, JSON.stringify(contacts));
22 | }
23 |
24 | return contacts;
25 | }
26 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useTheme } from "next-themes"
5 | import { Button } from "@/components/ui/button"
6 | import { Icons } from "./icons"
7 |
8 | export function ThemeToggle() {
9 | const { setTheme, theme } = useTheme()
10 |
11 | return (
12 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface Message {
2 | id: string;
3 | content: string;
4 | htmlContent?: string;
5 | sender: "me" | "system" | string;
6 | timestamp: string;
7 | type?: "silenced";
8 | mentions?: { id: string; name: string; }[];
9 | reactions?: Reaction[];
10 | }
11 |
12 | export interface Conversation {
13 | id: string;
14 | name?: string;
15 | recipients: Recipient[];
16 | messages: Message[];
17 | lastMessageTime: string;
18 | unreadCount: number;
19 | pinned?: boolean;
20 | isTyping?: boolean;
21 | hideAlerts?: boolean;
22 | }
23 |
24 | export interface Recipient {
25 | id: string;
26 | name: string;
27 | avatar?: string;
28 | bio?: string;
29 | title?: string;
30 | }
31 |
32 | export type ReactionType = 'heart' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question';
33 |
34 | export interface Reaction {
35 | type: ReactionType;
36 | sender: string;
37 | timestamp: string;
38 | }
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/validate-contact/route.ts:
--------------------------------------------------------------------------------
1 | import { initLogger, invoke, wrapTraced } from "braintrust";
2 | import { NextResponse } from 'next/server';
3 |
4 | initLogger({
5 | projectName: "messages",
6 | apiKey: process.env.BRAINTRUST_API_KEY,
7 | asyncFlush: true,
8 | });
9 |
10 | export async function POST(req: Request) {
11 | try {
12 | const body = await req.json();
13 | const { name } = body;
14 | const data = await handleRequest(name);
15 | return NextResponse.json(data);
16 |
17 | } catch (error) {
18 | console.error("Error in API route:", error);
19 | return NextResponse.json({ error: String(error) }, { status: 500 });
20 | }
21 | }
22 |
23 | const handleRequest = wrapTraced(async function handleRequest(name: string) {
24 | try {
25 | const result = await invoke({
26 | projectName: "messages",
27 | slug: "validate-name-317c",
28 | input: {
29 | name,
30 | },
31 | stream: false,
32 | });
33 | return result;
34 | } catch (error) {
35 | console.error("Error in handleRequest:", error);
36 | throw error;
37 | }
38 | });
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/reactions/heart-white.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/reactions/heart-gray.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { ThemeProvider } from "@/components/theme-provider";
3 | import { siteConfig } from "@/config/site";
4 | import "./globals.css";
5 | import { Toaster } from "@/components/ui/toaster";
6 |
7 | export const metadata: Metadata = {
8 | title: siteConfig.title,
9 | openGraph: {
10 | title: siteConfig.title,
11 | siteName: siteConfig.title,
12 | url: siteConfig.url,
13 | images: [
14 | {
15 | url: "/messages/api/og",
16 | width: 1200,
17 | height: 630,
18 | },
19 | ],
20 | },
21 | twitter: {
22 | card: "summary_large_image",
23 | title: siteConfig.title,
24 | images: ["/messages/api/og"],
25 | },
26 | };
27 |
28 | export default function RootLayout({
29 | children,
30 | }: {
31 | children: React.ReactNode;
32 | }) {
33 | return (
34 |
35 |
36 |
40 |
41 |
42 |
48 | {children}
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/components/swipe-actions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icons } from './icons';
3 |
4 | interface SwipeActionsProps {
5 | isOpen: boolean;
6 | onDelete: () => void;
7 | onPin: () => void;
8 | onHideAlerts: () => void;
9 | isPinned?: boolean;
10 | hideAlerts?: boolean;
11 | }
12 |
13 | export function SwipeActions({
14 | isOpen,
15 | onDelete,
16 | onPin,
17 | onHideAlerts,
18 | isPinned = false,
19 | hideAlerts = false,
20 | }: SwipeActionsProps) {
21 | return (
22 |
29 |
35 |
41 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/public/reactions/like-gray.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/like-white.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-heart-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-heart-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-blue-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-blue-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-pinned-light-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/tiptap.css:
--------------------------------------------------------------------------------
1 | .ProseMirror {
2 | > * + * {
3 | margin-top: 0.75em;
4 | }
5 |
6 | &:focus {
7 | outline: none;
8 | }
9 |
10 | @apply text-foreground;
11 | }
12 |
13 | .suggestion {
14 | color: #6b7280; /* gray-500 */
15 | }
16 |
17 | /* Mention node styling */
18 | .ProseMirror .mention-node {
19 | color: #0A7CFF;
20 | font-weight: 500;
21 | position: relative;
22 | display: inline-block;
23 | background: #0A7CFF -webkit-gradient(linear, 100% 0, 0 0, from(#0A7CFF), color-stop(0.5, #ffffff), to(#0A7CFF));
24 | background-position: -4rem top;
25 | background-repeat: no-repeat;
26 | -webkit-background-clip: text;
27 | -webkit-text-fill-color: transparent;
28 | background-size: 4rem 100%;
29 | transform-origin: bottom;
30 | }
31 |
32 | .ProseMirror .mention-node:not(.shimmer-done) {
33 | animation:
34 | shimmer 2.2s cubic-bezier(0.4, 0.0, 0.2, 1) forwards,
35 | bounce 2.2s cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
36 | animation-iteration-count: 1, 1;
37 | animation-play-state: running;
38 | animation-fill-mode: forwards;
39 | }
40 |
41 | .ProseMirror .mention-node.shimmer-done {
42 | animation: none;
43 | transform: translateY(0);
44 | }
45 |
46 | @keyframes shimmer {
47 | 0% {
48 | background-position: -4rem top;
49 | }
50 | 100% {
51 | background-position: 12.5rem top;
52 | }
53 | }
54 |
55 | @keyframes bounce {
56 | 0% {
57 | transform: translateY(0) scale(1);
58 | }
59 | 30% {
60 | transform: translateY(-1px) scale(1.02);
61 | }
62 | 100% {
63 | transform: translateY(0) scale(1);
64 | }
65 | }
66 |
67 | .ProseMirror .mention-node::after {
68 | display: none;
69 | }
70 |
71 | /* Hide the default prosemirror menu */
72 | .ProseMirror-menubar {
73 | display: none;
74 | }
75 |
76 | /* Placeholder styling */
77 | .ProseMirror p.is-editor-empty:first-child::before {
78 | @apply text-muted-foreground;
79 | content: attr(data-placeholder);
80 | float: left;
81 | height: 0;
82 | pointer-events: none;
83 | }
84 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-heart-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-heart-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-blue-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-blue-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-pinned-light-heart.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/question-white.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "socratichat",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/openai": "^1.0.4",
13 | "@emoji-mart/data": "^1.2.1",
14 | "@emoji-mart/react": "^1.1.1",
15 | "@fortawesome/fontawesome-svg-core": "^6.7.1",
16 | "@fortawesome/free-solid-svg-icons": "^6.7.1",
17 | "@fortawesome/react-fontawesome": "^0.2.2",
18 | "@radix-ui/react-avatar": "^1.1.1",
19 | "@radix-ui/react-context-menu": "^2.2.2",
20 | "@radix-ui/react-dialog": "^1.1.4",
21 | "@radix-ui/react-label": "^2.1.0",
22 | "@radix-ui/react-popover": "^1.1.2",
23 | "@radix-ui/react-scroll-area": "^1.2.1",
24 | "@radix-ui/react-slot": "^1.1.0",
25 | "@radix-ui/react-switch": "^1.1.2",
26 | "@radix-ui/react-toast": "^1.2.3",
27 | "@tiptap/extension-mention": "^2.10.3",
28 | "@tiptap/extension-placeholder": "^2.10.3",
29 | "@tiptap/pm": "^2.10.3",
30 | "@tiptap/react": "^2.10.3",
31 | "@tiptap/starter-kit": "^2.10.3",
32 | "@types/uuid": "^10.0.0",
33 | "ai": "^4.0.3",
34 | "autoevals": "^0.0.115",
35 | "braintrust": "^0.0.171",
36 | "class-variance-authority": "^0.7.0",
37 | "clsx": "^2.1.1",
38 | "cmdk": "^1.0.0",
39 | "date-fns": "^4.1.0",
40 | "lucide-react": "^0.460.0",
41 | "next": "15.0.5",
42 | "next-themes": "^0.4.3",
43 | "openai": "^4.73.0",
44 | "react": "18.2.0",
45 | "react-dom": "18.2.0",
46 | "react-swipeable": "^7.0.2",
47 | "tailwind-merge": "^2.5.4",
48 | "tailwindcss-animate": "^1.0.7",
49 | "uuid": "^11.0.3",
50 | "vaul": "^1.1.2"
51 | },
52 | "devDependencies": {
53 | "@types/node": "^20",
54 | "@types/react": "^18",
55 | "@types/react-dom": "^18",
56 | "eslint": "^8",
57 | "eslint-config-next": "15.0.5",
58 | "postcss": "^8",
59 | "tailwindcss": "^3.4.1",
60 | "typescript": "^5"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/public/reactions/question-gray.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | safelist: ["mention-node"],
11 | theme: {
12 | extend: {
13 | colors: {
14 | background: "hsl(var(--background))",
15 | foreground: "hsl(var(--foreground))",
16 | card: {
17 | DEFAULT: "hsl(var(--card))",
18 | foreground: "hsl(var(--card-foreground))",
19 | },
20 | popover: {
21 | DEFAULT: "hsl(var(--popover))",
22 | foreground: "hsl(var(--popover-foreground))",
23 | },
24 | primary: {
25 | DEFAULT: "hsl(var(--primary))",
26 | foreground: "hsl(var(--primary-foreground))",
27 | },
28 | secondary: {
29 | DEFAULT: "hsl(var(--secondary))",
30 | foreground: "hsl(var(--secondary-foreground))",
31 | },
32 | muted: {
33 | DEFAULT: "hsl(var(--muted))",
34 | foreground: "hsl(var(--muted-foreground))",
35 | },
36 | accent: {
37 | DEFAULT: "hsl(var(--accent))",
38 | foreground: "hsl(var(--accent-foreground))",
39 | },
40 | destructive: {
41 | DEFAULT: "hsl(var(--destructive))",
42 | foreground: "hsl(var(--destructive-foreground))",
43 | },
44 | border: "hsl(var(--border))",
45 | input: "hsl(var(--input))",
46 | ring: "hsl(var(--ring))",
47 | chart: {
48 | "1": "hsl(var(--chart-1))",
49 | "2": "hsl(var(--chart-2))",
50 | "3": "hsl(var(--chart-3))",
51 | "4": "hsl(var(--chart-4))",
52 | "5": "hsl(var(--chart-5))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | },
61 | },
62 | plugins: [require("tailwindcss-animate")],
63 | } satisfies Config;
64 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/public/reactions/dislike-white.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/dislike-gray.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/search-bar.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { Icons } from "./icons";
3 |
4 | interface SearchBarProps {
5 | value: string;
6 | onChange: (value: string) => void;
7 | }
8 |
9 | export function SearchBar({ value, onChange }: SearchBarProps) {
10 | const justBlurred = useRef(false);
11 |
12 | useEffect(() => {
13 | const handleGlobalEscape = (e: KeyboardEvent) => {
14 | if (e.key === "Escape") {
15 | const searchInput = document.querySelector(
16 | 'input[placeholder="Search"]'
17 | );
18 | if (
19 | document.activeElement !== searchInput &&
20 | value &&
21 | !justBlurred.current
22 | ) {
23 | onChange("");
24 | }
25 | justBlurred.current = false;
26 | }
27 | };
28 |
29 | window.addEventListener("keydown", handleGlobalEscape);
30 | return () => window.removeEventListener("keydown", handleGlobalEscape);
31 | }, [value, onChange]);
32 |
33 | return (
34 |
35 |
36 |
37 | onChange(e.target.value)}
41 | onKeyDown={(e) => {
42 | if (e.key === "Escape") {
43 | e.preventDefault();
44 | if (document.activeElement === e.currentTarget) {
45 | justBlurred.current = true;
46 | e.currentTarget.blur();
47 | setTimeout(() => {
48 | justBlurred.current = false;
49 | }, 0);
50 | }
51 | }
52 | }}
53 | placeholder="Search"
54 | className="w-full pl-8 pr-8 py-0.5 rounded-lg text-base sm:text-sm placeholder:text-sm placeholder:text-muted-foreground focus:outline-none bg-[#E8E8E7] dark:bg-[#353533]"
55 | />
56 | {value && (
57 |
64 | )}
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-like-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-like-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-blue-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-blue-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-pinned-light-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-like-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-like-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-blue-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-blue-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-pinned-light-like.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-question-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-question-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-blue-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-blue-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-light-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/left-pinned-light-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-question-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-question-overlay.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-blue-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-light-blue-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/right-pinned-light-question.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/reactions/emphasize-white.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/reactions/emphasize-gray.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-dislike-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-dislike-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-blue-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-blue-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-light-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/left-pinned-light-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-dislike-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-dislike-overlay.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-dark-blue-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-light-blue-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/reactions/right-pinned-light-dislike.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/components/nav.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from "./icons";
2 | import { useEffect } from "react";
3 | import { cn } from "@/lib/utils";
4 |
5 | interface NavProps {
6 | onNewChat: () => void;
7 | isMobileView: boolean;
8 | isScrolled?: boolean;
9 | }
10 |
11 | export function Nav({ onNewChat, isMobileView, isScrolled }: NavProps) {
12 | // Keyboard shortcut for creating a new chat
13 | useEffect(() => {
14 | const handleKeyDown = (e: KeyboardEvent) => {
15 | // Don't trigger if typing in an input, if command/meta key is pressed,
16 | // or if the TipTap editor is focused
17 | if (
18 | document.activeElement?.tagName === "INPUT" ||
19 | e.metaKey ||
20 | document.querySelector(".ProseMirror")?.contains(document.activeElement)
21 | ) {
22 | return;
23 | }
24 |
25 | if (e.key === "n") {
26 | e.preventDefault();
27 | onNewChat();
28 | }
29 | };
30 |
31 | document.addEventListener("keydown", handleKeyDown);
32 | return () => document.removeEventListener("keydown", handleKeyDown);
33 | }, [onNewChat]);
34 |
35 | return (
36 | <>
37 |
44 |
45 |
52 |
55 |
58 |
59 |
68 |
69 | >
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/lib/sound-effects.ts:
--------------------------------------------------------------------------------
1 | class SoundEffectPlayer {
2 | private static instance: SoundEffectPlayer;
3 | private sentSound: HTMLAudioElement | null = null;
4 | private receivedSound: HTMLAudioElement | null = null;
5 | private unreadSound: HTMLAudioElement | null = null;
6 | private reactionSound: HTMLAudioElement | null = null;
7 | private enabled: boolean = true;
8 | private isMobile: boolean = false;
9 |
10 | private constructor() {
11 | if (typeof window !== 'undefined') {
12 | // Check if device is mobile
13 | this.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
14 |
15 | // Get sound preference from localStorage, default to !isMobile if not set
16 | const storedEnabled = localStorage.getItem('soundEnabled');
17 | this.enabled = storedEnabled !== null ? storedEnabled === 'true' : !this.isMobile;
18 |
19 | this.sentSound = new Audio('/messages/sound-effects/sent.m4a');
20 | this.receivedSound = new Audio('/messages/sound-effects/received.m4a');
21 | this.unreadSound = new Audio('/messages/sound-effects/unread.m4a');
22 | this.reactionSound = new Audio('/messages/sound-effects/reaction.m4a');
23 | }
24 | }
25 |
26 | public static getInstance(): SoundEffectPlayer {
27 | if (!SoundEffectPlayer.instance) {
28 | SoundEffectPlayer.instance = new SoundEffectPlayer();
29 | }
30 | return SoundEffectPlayer.instance;
31 | }
32 |
33 | public playSentSound() {
34 | if (this.enabled && typeof window !== 'undefined' && this.sentSound) {
35 | this.sentSound.currentTime = 0;
36 | this.sentSound.play().catch(() => {
37 | // Silently handle autoplay restrictions
38 | });
39 | }
40 | }
41 |
42 | public playUnreadSound() {
43 | if (this.enabled && typeof window !== 'undefined' && this.unreadSound) {
44 | this.unreadSound.currentTime = 0;
45 | this.unreadSound.play().catch(() => {
46 | // Silently handle autoplay restrictions
47 | });
48 | }
49 | }
50 |
51 | public playReceivedSound() {
52 | if (this.enabled && typeof window !== 'undefined' && this.receivedSound) {
53 | this.receivedSound.currentTime = 0;
54 | this.receivedSound.play().catch(() => {
55 | // Silently handle autoplay restrictions
56 | });
57 | }
58 | }
59 |
60 | public playReactionSound() {
61 | if (this.enabled && typeof window !== 'undefined' && this.reactionSound) {
62 | this.reactionSound.currentTime = 0;
63 | this.reactionSound.play().catch(() => {
64 | // Silently handle autoplay restrictions
65 | });
66 | }
67 | }
68 |
69 | public toggleSound() {
70 | this.enabled = !this.enabled;
71 | if (typeof window !== 'undefined') {
72 | localStorage.setItem('soundEnabled', this.enabled.toString());
73 | }
74 | }
75 |
76 | public isEnabled(): boolean {
77 | return this.enabled;
78 | }
79 |
80 | public setEnabled(enabled: boolean) {
81 | this.enabled = enabled;
82 | }
83 | }
84 |
85 | export const soundEffects = SoundEffectPlayer.getInstance();
86 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-emphasize-overlay.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/reactions/left-light-emphasize-overlay.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/reactions/left-light-blue-emphasize.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-blue-emphasize.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/reactions/left-dark-emphasize.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------