mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-08-10 09:48:40 +00:00
Merge branch 'ItzCrazyKns:master' into master
This commit is contained in:
@ -1,5 +1,8 @@
|
|||||||
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
# 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc -->
|
||||||
|
|
||||||
|
[](https://discord.gg/26aArMy8tT)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Table of Contents <!-- omit in toc -->
|
## Table of Contents <!-- omit in toc -->
|
||||||
|
@ -9,12 +9,20 @@ export const loadAnthropicChatModels = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels = {
|
const chatModels = {
|
||||||
'claude-3-5-sonnet-20240620': {
|
'claude-3-5-sonnet-20241022': {
|
||||||
displayName: 'Claude 3.5 Sonnet',
|
displayName: 'Claude 3.5 Sonnet',
|
||||||
model: new ChatAnthropic({
|
model: new ChatAnthropic({
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
anthropicApiKey: anthropicApiKey,
|
anthropicApiKey: anthropicApiKey,
|
||||||
model: 'claude-3-5-sonnet-20240620',
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'claude-3-5-haiku-20241022': {
|
||||||
|
displayName: 'Claude 3.5 Haiku',
|
||||||
|
model: new ChatAnthropic({
|
||||||
|
temperature: 0.7,
|
||||||
|
anthropicApiKey: anthropicApiKey,
|
||||||
|
model: 'claude-3-5-haiku-20241022',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'claude-3-opus-20240229': {
|
'claude-3-opus-20240229': {
|
||||||
|
@ -9,6 +9,19 @@ export const loadGroqChatModels = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const chatModels = {
|
const chatModels = {
|
||||||
|
'llama-3.3-70b-versatile': {
|
||||||
|
displayName: 'Llama 3.3 70B',
|
||||||
|
model: new ChatOpenAI(
|
||||||
|
{
|
||||||
|
openAIApiKey: groqApiKey,
|
||||||
|
modelName: 'llama-3.3-70b-versatile',
|
||||||
|
temperature: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: 'https://api.groq.com/openai/v1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
'llama-3.2-3b-preview': {
|
'llama-3.2-3b-preview': {
|
||||||
displayName: 'Llama 3.2 3B',
|
displayName: 'Llama 3.2 3B',
|
||||||
model: new ChatOpenAI(
|
model: new ChatOpenAI(
|
||||||
@ -48,19 +61,6 @@ export const loadGroqChatModels = async () => {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
'llama-3.1-70b-versatile': {
|
|
||||||
displayName: 'Llama 3.1 70B',
|
|
||||||
model: new ChatOpenAI(
|
|
||||||
{
|
|
||||||
openAIApiKey: groqApiKey,
|
|
||||||
modelName: 'llama-3.1-70b-versatile',
|
|
||||||
temperature: 0.7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
baseURL: 'https://api.groq.com/openai/v1',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
'llama-3.1-8b-instant': {
|
'llama-3.1-8b-instant': {
|
||||||
displayName: 'Llama 3.1 8B',
|
displayName: 'Llama 3.1 8B',
|
||||||
model: new ChatOpenAI(
|
model: new ChatOpenAI(
|
||||||
@ -113,19 +113,6 @@ export const loadGroqChatModels = async () => {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
'gemma-7b-it': {
|
|
||||||
displayName: 'Gemma 7B',
|
|
||||||
model: new ChatOpenAI(
|
|
||||||
{
|
|
||||||
openAIApiKey: groqApiKey,
|
|
||||||
modelName: 'gemma-7b-it',
|
|
||||||
temperature: 0.7,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
baseURL: 'https://api.groq.com/openai/v1',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
'gemma2-9b-it': {
|
'gemma2-9b-it': {
|
||||||
displayName: 'Gemma2 9B',
|
displayName: 'Gemma2 9B',
|
||||||
model: new ChatOpenAI(
|
model: new ChatOpenAI(
|
||||||
|
@ -2,6 +2,7 @@ import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
|
|||||||
import { getKeepAlive, getOllamaApiEndpoint } from '../../config';
|
import { getKeepAlive, getOllamaApiEndpoint } from '../../config';
|
||||||
import logger from '../../utils/logger';
|
import logger from '../../utils/logger';
|
||||||
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
import { ChatOllama } from '@langchain/community/chat_models/ollama';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export const loadOllamaChatModels = async () => {
|
export const loadOllamaChatModels = async () => {
|
||||||
const ollamaEndpoint = getOllamaApiEndpoint();
|
const ollamaEndpoint = getOllamaApiEndpoint();
|
||||||
@ -10,13 +11,13 @@ export const loadOllamaChatModels = async () => {
|
|||||||
if (!ollamaEndpoint) return {};
|
if (!ollamaEndpoint) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${ollamaEndpoint}/api/tags`, {
|
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { models: ollamaModels } = (await response.json()) as any;
|
const { models: ollamaModels } = response.data;
|
||||||
|
|
||||||
const chatModels = ollamaModels.reduce((acc, model) => {
|
const chatModels = ollamaModels.reduce((acc, model) => {
|
||||||
acc[model.model] = {
|
acc[model.model] = {
|
||||||
@ -45,13 +46,13 @@ export const loadOllamaEmbeddingsModels = async () => {
|
|||||||
if (!ollamaEndpoint) return {};
|
if (!ollamaEndpoint) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${ollamaEndpoint}/api/tags`, {
|
const response = await axios.get(`${ollamaEndpoint}/api/tags`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { models: ollamaModels } = (await response.json()) as any;
|
const { models: ollamaModels } = response.data;
|
||||||
|
|
||||||
const embeddingsModels = ollamaModels.reduce((acc, model) => {
|
const embeddingsModels = ollamaModels.reduce((acc, model) => {
|
||||||
acc[model.model] = {
|
acc[model.model] = {
|
||||||
|
@ -211,7 +211,11 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
const documents = res.results.map(
|
const documents = res.results.map(
|
||||||
(result) =>
|
(result) =>
|
||||||
new Document({
|
new Document({
|
||||||
pageContent: result.content,
|
pageContent:
|
||||||
|
result.content ||
|
||||||
|
(this.config.activeEngines.includes('youtube')
|
||||||
|
? result.title
|
||||||
|
: '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: result.title,
|
title: result.title,
|
||||||
url: result.url,
|
url: result.url,
|
||||||
@ -414,7 +418,10 @@ class MetaSearchAgent implements MetaSearchAgentType {
|
|||||||
|
|
||||||
private processDocs(docs: Document[]) {
|
private processDocs(docs: Document[]) {
|
||||||
return docs
|
return docs
|
||||||
.map((_, index) => `${index + 1}. ${docs[index].pageContent}`)
|
.map(
|
||||||
|
(_, index) =>
|
||||||
|
`${index + 1}. ${docs[index].metadata.title} ${docs[index].pageContent}`,
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import type { Embeddings } from '@langchain/core/embeddings';
|
|||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import db from '../db';
|
import db from '../db';
|
||||||
import { chats, messages as messagesSchema } from '../db/schema';
|
import { chats, messages as messagesSchema } from '../db/schema';
|
||||||
import { eq, asc, gt } from 'drizzle-orm';
|
import { eq, asc, gt, and } from 'drizzle-orm';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { getFileDetails } from '../utils/files';
|
import { getFileDetails } from '../utils/files';
|
||||||
import MetaSearchAgent, {
|
import MetaSearchAgent, {
|
||||||
@ -238,7 +238,12 @@ export const handleMessage = async (
|
|||||||
} else {
|
} else {
|
||||||
await db
|
await db
|
||||||
.delete(messagesSchema)
|
.delete(messagesSchema)
|
||||||
.where(gt(messagesSchema.id, messageExists.id))
|
.where(
|
||||||
|
and(
|
||||||
|
gt(messagesSchema.id, messageExists.id),
|
||||||
|
eq(messagesSchema.chatId, parsedMessage.chatId),
|
||||||
|
),
|
||||||
|
)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -9,7 +9,9 @@ import crypto from 'crypto';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getSuggestions } from '@/lib/actions';
|
import { getSuggestions } from '@/lib/actions';
|
||||||
import Error from 'next/error';
|
import { Settings } from 'lucide-react';
|
||||||
|
import SettingsDialog from './SettingsDialog';
|
||||||
|
import NextError from 'next/error';
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
@ -32,17 +34,38 @@ const useSocket = (
|
|||||||
setIsWSReady: (ready: boolean) => void,
|
setIsWSReady: (ready: boolean) => void,
|
||||||
setError: (error: boolean) => void,
|
setError: (error: boolean) => void,
|
||||||
) => {
|
) => {
|
||||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const retryCountRef = useRef(0);
|
||||||
|
const isCleaningUpRef = useRef(false);
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_BACKOFF = 1000; // 1 second
|
||||||
|
|
||||||
|
const getBackoffDelay = (retryCount: number) => {
|
||||||
|
return Math.min(INITIAL_BACKOFF * Math.pow(2, retryCount), 10000); // Cap at 10 seconds
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ws) {
|
const connectWs = async () => {
|
||||||
const connectWs = async () => {
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let chatModel = localStorage.getItem('chatModel');
|
let chatModel = localStorage.getItem('chatModel');
|
||||||
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
let embeddingModel = localStorage.getItem('embeddingModel');
|
let embeddingModel = localStorage.getItem('embeddingModel');
|
||||||
let embeddingModelProvider = localStorage.getItem(
|
let embeddingModelProvider = localStorage.getItem(
|
||||||
'embeddingModelProvider',
|
'embeddingModelProvider',
|
||||||
);
|
);
|
||||||
|
let openAIBaseURL =
|
||||||
|
chatModelProvider === 'custom_openai'
|
||||||
|
? localStorage.getItem('openAIBaseURL')
|
||||||
|
: null;
|
||||||
|
let openAIPIKey =
|
||||||
|
chatModelProvider === 'custom_openai'
|
||||||
|
? localStorage.getItem('openAIApiKey')
|
||||||
|
: null;
|
||||||
|
|
||||||
const providers = await fetch(
|
const providers = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/models`,
|
`${process.env.NEXT_PUBLIC_API_URL}/models`,
|
||||||
@ -51,7 +74,13 @@ const useSocket = (
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).then(async (res) => await res.json());
|
).then(async (res) => {
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch models: ${res.status} ${res.statusText}`,
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!chatModel ||
|
!chatModel ||
|
||||||
@ -62,16 +91,18 @@ const useSocket = (
|
|||||||
if (!chatModel || !chatModelProvider) {
|
if (!chatModel || !chatModelProvider) {
|
||||||
const chatModelProviders = providers.chatModelProviders;
|
const chatModelProviders = providers.chatModelProviders;
|
||||||
|
|
||||||
chatModelProvider = Object.keys(chatModelProviders)[0];
|
chatModelProvider =
|
||||||
|
chatModelProvider || Object.keys(chatModelProviders)[0];
|
||||||
|
|
||||||
if (chatModelProvider === 'custom_openai') {
|
if (chatModelProvider === 'custom_openai') {
|
||||||
toast.error(
|
toast.error(
|
||||||
'Seems like you are using the custom OpenAI provider, please open the settings and configure the API key and base URL',
|
'Seems like you are using the custom OpenAI provider, please open the settings and enter a model name to use.',
|
||||||
);
|
);
|
||||||
setError(true);
|
setError(true);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!chatModelProviders ||
|
!chatModelProviders ||
|
||||||
Object.keys(chatModelProviders).length === 0
|
Object.keys(chatModelProviders).length === 0
|
||||||
@ -108,18 +139,42 @@ const useSocket = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
Object.keys(chatModelProviders).length > 0 &&
|
Object.keys(chatModelProviders).length > 0 &&
|
||||||
!chatModelProviders[chatModelProvider]
|
(((!openAIBaseURL || !openAIPIKey) &&
|
||||||
|
chatModelProvider === 'custom_openai') ||
|
||||||
|
!chatModelProviders[chatModelProvider])
|
||||||
) {
|
) {
|
||||||
chatModelProvider = Object.keys(chatModelProviders)[0];
|
const chatModelProvidersKeys = Object.keys(chatModelProviders);
|
||||||
|
chatModelProvider =
|
||||||
|
chatModelProvidersKeys.find(
|
||||||
|
(key) => Object.keys(chatModelProviders[key]).length > 0,
|
||||||
|
) || chatModelProvidersKeys[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
chatModelProvider === 'custom_openai' &&
|
||||||
|
(!openAIBaseURL || !openAIPIKey)
|
||||||
|
) {
|
||||||
|
toast.error(
|
||||||
|
'Seems like you are using the custom OpenAI provider, please open the settings and configure the API key and base URL',
|
||||||
|
);
|
||||||
|
setError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem('chatModelProvider', chatModelProvider);
|
localStorage.setItem('chatModelProvider', chatModelProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
chatModelProvider &&
|
chatModelProvider &&
|
||||||
chatModelProvider != 'custom_openai' &&
|
(!openAIBaseURL || !openAIPIKey) &&
|
||||||
!chatModelProviders[chatModelProvider][chatModel]
|
!chatModelProviders[chatModelProvider][chatModel]
|
||||||
) {
|
) {
|
||||||
chatModel = Object.keys(chatModelProviders[chatModelProvider])[0];
|
chatModel = Object.keys(
|
||||||
|
chatModelProviders[
|
||||||
|
Object.keys(chatModelProviders[chatModelProvider]).length > 0
|
||||||
|
? chatModelProvider
|
||||||
|
: Object.keys(chatModelProviders)[0]
|
||||||
|
],
|
||||||
|
)[0];
|
||||||
localStorage.setItem('chatModel', chatModel);
|
localStorage.setItem('chatModel', chatModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +223,7 @@ const useSocket = (
|
|||||||
wsURL.search = searchParams.toString();
|
wsURL.search = searchParams.toString();
|
||||||
|
|
||||||
const ws = new WebSocket(wsURL.toString());
|
const ws = new WebSocket(wsURL.toString());
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (ws.readyState !== 1) {
|
if (ws.readyState !== 1) {
|
||||||
@ -183,11 +239,16 @@ const useSocket = (
|
|||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (ws.readyState === 1) {
|
if (ws.readyState === 1) {
|
||||||
setIsWSReady(true);
|
setIsWSReady(true);
|
||||||
|
setError(false);
|
||||||
|
if (retryCountRef.current > 0) {
|
||||||
|
toast.success('Connection restored.');
|
||||||
|
}
|
||||||
|
retryCountRef.current = 0;
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
}, 5);
|
}, 5);
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('[DEBUG] opened');
|
console.debug(new Date(), 'ws:connected');
|
||||||
}
|
}
|
||||||
if (data.type === 'error') {
|
if (data.type === 'error') {
|
||||||
toast.error(data.data);
|
toast.error(data.data);
|
||||||
@ -196,24 +257,68 @@ const useSocket = (
|
|||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
setError(true);
|
setIsWSReady(false);
|
||||||
toast.error('WebSocket connection error.');
|
toast.error('WebSocket connection error.');
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
setError(true);
|
setIsWSReady(false);
|
||||||
console.log('[DEBUG] closed');
|
console.debug(new Date(), 'ws:disconnected');
|
||||||
|
if (!isCleaningUpRef.current) {
|
||||||
|
toast.error('Connection lost. Attempting to reconnect...');
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(new Date(), 'ws:error', error);
|
||||||
|
setIsWSReady(false);
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
setWs(ws);
|
const attemptReconnect = () => {
|
||||||
};
|
retryCountRef.current += 1;
|
||||||
|
|
||||||
connectWs();
|
if (retryCountRef.current > MAX_RETRIES) {
|
||||||
}
|
console.debug(new Date(), 'ws:max_retries');
|
||||||
}, [ws, url, setIsWSReady, setError]);
|
setError(true);
|
||||||
|
toast.error(
|
||||||
|
'Unable to connect to server after multiple attempts. Please refresh the page to try again.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return ws;
|
const backoffDelay = getBackoffDelay(retryCountRef.current);
|
||||||
|
console.debug(
|
||||||
|
new Date(),
|
||||||
|
`ws:retry attempt=${retryCountRef.current}/${MAX_RETRIES} delay=${backoffDelay}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
connectWs();
|
||||||
|
}, backoffDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
connectWs();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
isCleaningUpRef.current = true;
|
||||||
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [url, setIsWSReady, setError]);
|
||||||
|
|
||||||
|
return wsRef.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMessages = async (
|
const loadMessages = async (
|
||||||
@ -257,7 +362,7 @@ const loadMessages = async (
|
|||||||
return [msg.role, msg.content];
|
return [msg.role, msg.content];
|
||||||
}) as [string, string][];
|
}) as [string, string][];
|
||||||
|
|
||||||
console.log('[DEBUG] messages loaded');
|
console.debug(new Date(), 'app:messages_loaded');
|
||||||
|
|
||||||
document.title = messages[0].content;
|
document.title = messages[0].content;
|
||||||
|
|
||||||
@ -310,6 +415,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
const [notFound, setNotFound] = useState(false);
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
chatId &&
|
chatId &&
|
||||||
@ -339,7 +446,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
if (ws?.readyState === 1) {
|
if (ws?.readyState === 1) {
|
||||||
ws.close();
|
ws.close();
|
||||||
console.log('[DEBUG] closed');
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -354,12 +461,18 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMessagesLoaded && isWSReady) {
|
if (isMessagesLoaded && isWSReady) {
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
console.log('[DEBUG] ready');
|
console.debug(new Date(), 'app:ready');
|
||||||
|
} else {
|
||||||
|
setIsReady(false);
|
||||||
}
|
}
|
||||||
}, [isMessagesLoaded, isWSReady]);
|
}, [isMessagesLoaded, isWSReady]);
|
||||||
|
|
||||||
const sendMessage = async (message: string, messageId?: string) => {
|
const sendMessage = async (message: string, messageId?: string) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
toast.error('Cannot send message while disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessageAppeared(false);
|
setMessageAppeared(false);
|
||||||
@ -370,7 +483,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
|
||||||
ws?.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
message: {
|
message: {
|
||||||
@ -514,17 +627,26 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
|||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
<div className="relative">
|
||||||
<p className="dark:text-white/70 text-black/70 text-sm">
|
<div className="absolute w-full flex flex-row items-center justify-end mr-5 mt-5">
|
||||||
Failed to connect to the server. Please try again later.
|
<Settings
|
||||||
</p>
|
className="cursor-pointer lg:hidden"
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||||
|
<p className="dark:text-white/70 text-black/70 text-sm">
|
||||||
|
Failed to connect to the server. Please try again later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<SettingsDialog isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isReady ? (
|
return isReady ? (
|
||||||
notFound ? (
|
notFound ? (
|
||||||
<Error statusCode={404} />
|
<NextError statusCode={404} />
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react';
|
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
||||||
import 'yet-another-react-lightbox/styles.css';
|
import 'yet-another-react-lightbox/styles.css';
|
||||||
import { Message } from './ChatWindow';
|
import { Message } from './ChatWindow';
|
||||||
@ -35,6 +35,8 @@ const Searchvideos = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [slides, setSlides] = useState<VideoSlide[]>([]);
|
const [slides, setSlides] = useState<VideoSlide[]>([]);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const videoRefs = useRef<(HTMLIFrameElement | null)[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -182,18 +184,39 @@ const Searchvideos = ({
|
|||||||
open={open}
|
open={open}
|
||||||
close={() => setOpen(false)}
|
close={() => setOpen(false)}
|
||||||
slides={slides}
|
slides={slides}
|
||||||
|
index={currentIndex}
|
||||||
|
on={{
|
||||||
|
view: ({ index }) => {
|
||||||
|
const previousIframe = videoRefs.current[currentIndex];
|
||||||
|
if (previousIframe?.contentWindow) {
|
||||||
|
previousIframe.contentWindow.postMessage(
|
||||||
|
'{"event":"command","func":"pauseVideo","args":""}',
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIndex(index);
|
||||||
|
},
|
||||||
|
}}
|
||||||
render={{
|
render={{
|
||||||
slide: ({ slide }) =>
|
slide: ({ slide }) => {
|
||||||
slide.type === 'video-slide' ? (
|
const index = slides.findIndex((s) => s === slide);
|
||||||
|
return slide.type === 'video-slide' ? (
|
||||||
<div className="h-full w-full flex flex-row items-center justify-center">
|
<div className="h-full w-full flex flex-row items-center justify-center">
|
||||||
<iframe
|
<iframe
|
||||||
src={slide.iframe_src}
|
src={`${slide.iframe_src}${slide.iframe_src.includes('?') ? '&' : '?'}enablejsapi=1`}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) {
|
||||||
|
videoRefs.current[index] = el;
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="aspect-video max-h-[95vh] w-[95vw] rounded-2xl md:w-[80vw]"
|
className="aspect-video max-h-[95vh] w-[95vw] rounded-2xl md:w-[80vw]"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null,
|
) : null;
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
Reference in New Issue
Block a user