Merge pull request #1454 from MODSetter/dev_mod

feat(release):  v0.0.26
This commit is contained in:
Rohan Verma 2026-05-31 19:31:29 -07:00 committed by GitHub
commit c44263e3d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 6318 additions and 4835 deletions

View file

@ -41,6 +41,7 @@ NotebookLM es una de las mejores y más útiles plataformas de IA que existen, p
- **Sin Dependencia de Proveedores** - Configura cualquier modelo LLM, de imagen, TTS y STT. - **Sin Dependencia de Proveedores** - Configura cualquier modelo LLM, de imagen, TTS y STT.
- **25+ Fuentes de Datos Externas** - Agrega tus fuentes desde Google Drive, OneDrive, Dropbox, Notion y muchos otros servicios externos. - **25+ Fuentes de Datos Externas** - Agrega tus fuentes desde Google Drive, OneDrive, Dropbox, Notion y muchos otros servicios externos.
- **Soporte Multijugador en Tiempo Real** - Trabaja fácilmente con los miembros de tu equipo en un notebook compartido. - **Soporte Multijugador en Tiempo Real** - Trabaja fácilmente con los miembros de tu equipo en un notebook compartido.
- **Automatizaciones y Agentes de IA** - Ejecuta agentes de IA según una programación o actívalos en el momento en que un documento llega a una carpeta, y luego escribe los resultados de vuelta en Notion, Slack, Linear y Drive. Crea automatizaciones sin código solo describiéndolas en el chat.
- **Aplicación de Escritorio** - Obtén asistencia de IA en cualquier aplicación con Quick Assist, General Assist, Screenshot Assist y sincronización de carpetas locales. - **Aplicación de Escritorio** - Obtén asistencia de IA en cualquier aplicación con Quick Assist, General Assist, Screenshot Assist y sincronización de carpetas locales.
...y más por venir. ...y más por venir.
@ -76,48 +77,118 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
4. Una vez que todo esté indexado, pregunta lo que quieras (Casos de uso): 4. Una vez que todo esté indexado, pregunta lo que quieras (Casos de uso):
- Aplicación de Escritorio — General Assist **Aplicación de Escritorio** (extras nativos, además de todo lo de abajo, no un conjunto aparte)
- General Assist: abre SurfSense al instante desde cualquier aplicación con un atajo global.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p>
- Aplicación de Escritorio — Quick Assist - Quick Assist: selecciona texto en cualquier lugar y pide a la IA que lo explique, reescriba o actúe sobre él.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p>
- Aplicación de Escritorio — Screenshot Assist - Screenshot Assist: captura cualquier región de tu pantalla y pregunta a la IA sobre lo que contiene.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p>
- Aplicación de Escritorio — Watch Local Folder - Watch Local Folder: sincroniza automáticamente una carpeta local con tu base de conocimiento. Ideal para bóvedas de Obsidian.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p>
- Generación de videos **Estudio de Entregables**
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="Generación de Videos" /></p> - AI Report Generator: genera informes de investigación con citas y expórtalos a PDF, DOCX, HTML, LaTeX, EPUB, ODT o texto plano.
- Búsqueda básica y citaciones <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="AI Report Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="Búsqueda y Citación" /></p> - AI Podcast Generator: convierte cualquier documento o carpeta en un pódcast de IA con dos presentadores en menos de 20 segundos.
- QNA con mención de documentos <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="AI Podcast Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="QNA con Mención de Documentos" /></p> - AI Presentation & Video Maker: crea presentaciones editables y videos narrados a partir de tus fuentes.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="QNA con Mención de Documentos" /></p>
- Generación de informes y exportaciones (PDF, DOCX, HTML, LaTeX, EPUB, ODT, texto plano) <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="AI Presentation and Video Maker" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="Generación de Informes" /></p> - AI Image Generator: genera imágenes de alta calidad directamente desde tus chats y documentos.
- Generación de podcasts <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="AI Image Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="Generación de Podcasts" /></p> - AI Resume Builder: adapta tu currículum existente a cualquier descripción de empleo y supera el ATS.
Prueba indicaciones como estas:
- Generación de imágenes - "Adapta mi currículum a esta descripción de empleo para superar el ATS y conseguir una entrevista."
- "Optimiza mi currículum para ATS haciendo coincidir las palabras clave de esta oferta."
- "Reescribe los puntos de mi currículum para resaltar las habilidades que pide este puesto."
- "Compara mi currículum con esta descripción de empleo y enumera las carencias a corregir."
- "Escribe una carta de presentación a juego con mi currículum y esta descripción de empleo."
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="Generación de Imágenes" /></p> **Búsqueda y Chat**
- Y más próximamente. - Chat With Your PDFs & Docs: haz preguntas sobre todos tus archivos y obtén respuestas con citas en línea.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Chat With Your PDFs and Docs" /></p>
- AI Search With Citations: búsqueda híbrida semántica y por palabras clave en toda tu base de conocimiento.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="AI Search With Citations" /></p>
- Collaborative AI Chat: trabaja en conversaciones de IA con tu equipo en tiempo real.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeChatGif.gif" alt="Collaborative AI Chat" /></p>
- Comments & Mentions: comenta y menciona a tus compañeros en cualquier mensaje de IA.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeCommentsFlow.gif" alt="Comments and Mentions" /></p>
**Conectores e Integraciones**
- Connect & Sync Your Tools: sincroniza Notion, Slack, Google Drive, Gmail, GitHub, Linear y más de 25 fuentes en un único corpus consultable.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif" alt="Connect and Sync Your Tools" /></p>
- Chat With Uploaded Files: sube PDFs, documentos de Office, imágenes y audio. Consultables al instante.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif" alt="Chat With Uploaded Files" /></p>
- Connector Write-Back: deja que el agente publique los resultados de vuelta en Notion, Slack, Linear y Drive.
Prueba indicaciones como estas:
- "Publica este resumen de investigación en mi espacio de Notion."
- "Envía estos elementos de acción de la reunión a nuestro canal de Slack."
- "Crea un ticket de Jira a partir de este informe de error."
- "Abre una incidencia en Linear a partir de esta solicitud de función."
- "Guarda este informe generado en Google Drive como un documento."
- Obsidian & Knowledge Base Sync: mantén tu bóveda de Obsidian y tu base de conocimiento personal sincronizadas.
**Automatizaciones**
- Scheduled AI Workflows: ejecuta un agente según una programación: resúmenes diarios, boletines semanales, informes recurrentes.
Prueba indicaciones como estas:
- "Envíame cada mañana un resumen diario de los nuevos documentos en mi base de conocimiento."
- "Genera un informe de estado semanal a partir de mi Slack y Gmail cada viernes."
- "Ejecuta un informe mensual de análisis de la competencia y guárdalo en mi espacio de trabajo."
- "Resume mi actividad de GitHub y Linear en una actualización diaria de standup."
- "Crea un informe de investigación semanal recurrente sobre los temas que sigo."
- Event-Triggered Automations: lanza un agente en el momento en que un documento llega a una carpeta y publica el resultado en tus herramientas.
Prueba indicaciones como estas:
- "Cuando llegue un PDF a mi carpeta de Investigación, genera un resumen de IA con citas."
- "Cuando se añadan nuevas notas de reunión, conviértelas en actas con elementos de acción."
- "Cuando se suba una factura, extrae el proveedor, el total y la fecha de vencimiento en una tabla."
- "Cuando entre un contrato en mi carpeta Legal, señala los términos clave y las fechas de renovación."
- "Cuando se añada un currículum a Candidatos, evalúalo frente a la descripción del empleo."
- Chat-Built Automations: describe una automatización en lenguaje sencillo y SurfSense la crea por ti.
Prueba indicaciones como estas:
- "Crea un agente de IA que me envíe cada mañana un resumen de las nuevas páginas de Notion."
- "Crea una automatización sin código que publique un resumen de investigación semanal en Slack."
- "Configura un tomador de notas con IA que convierta las nuevas notas de reunión en actas."
- "Crea un flujo que extraiga los elementos de acción de las notas de reunión y asigne responsables."
- "Automatiza un resumen diario por correo a partir de mi Gmail y Google Drive."
### Auto-Hospedado ### Auto-Hospedado
@ -199,6 +270,7 @@ Todas las funciones operan contra tu espacio de búsqueda elegido, por lo que tu
| **Generación de Videos** | Resúmenes en video cinemáticos vía Veo 3 (solo Ultra) | Disponible (NotebookLM es mejor aquí, mejorando activamente) | | **Generación de Videos** | Resúmenes en video cinemáticos vía Veo 3 (solo Ultra) | Disponible (NotebookLM es mejor aquí, mejorando activamente) |
| **Generación de Presentaciones** | Diapositivas más atractivas pero no editables | Crea presentaciones editables basadas en diapositivas | | **Generación de Presentaciones** | Diapositivas más atractivas pero no editables | Crea presentaciones editables basadas en diapositivas |
| **Generación de Podcasts** | Resúmenes de audio con hosts e idiomas personalizables | Disponible con múltiples proveedores TTS (NotebookLM es mejor aquí, mejorando activamente) | | **Generación de Podcasts** | Resúmenes de audio con hosts e idiomas personalizables | Disponible con múltiples proveedores TTS (NotebookLM es mejor aquí, mejorando activamente) |
| **Automatizaciones y Agentes de IA** | No | Flujos de trabajo de IA programados, disparadores por eventos en documentos nuevos y automatizaciones sin código creadas por chat con escritura de vuelta a Notion, Slack, Linear y Jira |
| **Aplicación de Escritorio** | No | Aplicación nativa con General Assist, Quick Assist, Screenshot Assist y sincronización de carpetas locales | | **Aplicación de Escritorio** | No | Aplicación nativa con General Assist, Quick Assist, Screenshot Assist y sincronización de carpetas locales |
| **Extensión de Navegador** | No | Extensión multi-navegador para guardar cualquier página web, incluyendo páginas protegidas por autenticación | | **Extensión de Navegador** | No | Extensión multi-navegador para guardar cualquier página web, incluyendo páginas protegidas por autenticación |

View file

@ -41,6 +41,7 @@ NotebookLM वहाँ उपलब्ध सबसे अच्छे और
- **कोई विक्रेता लॉक-इन नहीं** - किसी भी LLM, इमेज, TTS और STT मॉडल को कॉन्फ़िगर करें। - **कोई विक्रेता लॉक-इन नहीं** - किसी भी LLM, इमेज, TTS और STT मॉडल को कॉन्फ़िगर करें।
- **25+ बाहरी डेटा स्रोत** - Google Drive, OneDrive, Dropbox, Notion और कई अन्य बाहरी सेवाओं से अपने स्रोत जोड़ें। - **25+ बाहरी डेटा स्रोत** - Google Drive, OneDrive, Dropbox, Notion और कई अन्य बाहरी सेवाओं से अपने स्रोत जोड़ें।
- **रीयल-टाइम मल्टीप्लेयर सपोर्ट** - एक साझा notebook में अपनी टीम के सदस्यों के साथ आसानी से काम करें। - **रीयल-टाइम मल्टीप्लेयर सपोर्ट** - एक साझा notebook में अपनी टीम के सदस्यों के साथ आसानी से काम करें।
- **AI ऑटोमेशन और एजेंट** - AI एजेंट को शेड्यूल पर चलाएं या जैसे ही कोई दस्तावेज़ किसी फ़ोल्डर में आए उसे ट्रिगर करें, फिर परिणाम वापस Notion, Slack, Linear और Drive में लिखें। चैट में बस वर्णन करके बिना-कोड ऑटोमेशन बनाएं।
- **डेस्कटॉप ऐप** - Quick Assist, General Assist, Screenshot Assist और लोकल फ़ोल्डर सिंक के साथ किसी भी एप्लिकेशन में AI सहायता प्राप्त करें। - **डेस्कटॉप ऐप** - Quick Assist, General Assist, Screenshot Assist और लोकल फ़ोल्डर सिंक के साथ किसी भी एप्लिकेशन में AI सहायता प्राप्त करें।
...और भी बहुत कुछ आने वाला है। ...और भी बहुत कुछ आने वाला है।
@ -76,48 +77,118 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
4. सब कुछ इंडेक्स हो जाने के बाद, कुछ भी पूछें (उपयोग के मामले): 4. सब कुछ इंडेक्स हो जाने के बाद, कुछ भी पूछें (उपयोग के मामले):
- डेस्कटॉप ऐप — General Assist **डेस्कटॉप ऐप** (नीचे दी गई सभी सुविधाओं के अलावा नेटिव एक्स्ट्रा, कोई अलग सेट नहीं)
- General Assist: किसी भी ऐप्लिकेशन से ग्लोबल शॉर्टकट के ज़रिए SurfSense तुरंत खोलें।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p>
- डेस्कटॉप ऐप — Quick Assist - Quick Assist: कहीं भी टेक्स्ट चुनें और AI से उसे समझाने, दोबारा लिखने या उस पर कार्रवाई करने को कहें।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p>
- डेस्कटॉप ऐप — Screenshot Assist - Screenshot Assist: अपनी स्क्रीन का कोई भी हिस्सा कैप्चर करें और AI से उसमें मौजूद चीज़ों के बारे में पूछें।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p>
- डेस्कटॉप ऐप — Watch Local Folder - Watch Local Folder: किसी लोकल फ़ोल्डर को अपने नॉलेज बेस के साथ अपने-आप सिंक करें। Obsidian vaults के लिए बढ़िया।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p>
- वीडियो जनरेशन **डिलीवरेबल स्टूडियो**
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="वीडियो जनरेशन" /></p> - AI Report Generator: उद्धरण सहित रिसर्च रिपोर्ट बनाएं और PDF, DOCX, HTML, LaTeX, EPUB, ODT या सादे टेक्स्ट में एक्सपोर्ट करें।
- बेसिक सर्च और उद्धरण <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="AI Report Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="सर्च और उद्धरण" /></p> - AI Podcast Generator: किसी भी दस्तावेज़ या फ़ोल्डर को 20 सेकंड से भी कम में दो-होस्ट वाले AI पॉडकास्ट में बदलें।
- दस्तावेज़ मेंशन QNA <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="AI Podcast Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="दस्तावेज़ मेंशन QNA" /></p> - AI Presentation & Video Maker: अपने स्रोतों से एडिट करने योग्य स्लाइड डेक और नैरेटेड वीडियो बनाएं।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="दस्तावेज़ मेंशन QNA" /></p>
- रिपोर्ट जनरेशन और एक्सपोर्ट (PDF, DOCX, HTML, LaTeX, EPUB, ODT, सादा टेक्स्ट) <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="AI Presentation and Video Maker" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="रिपोर्ट जनरेशन" /></p> - AI Image Generator: अपनी चैट और दस्तावेज़ों से सीधे उच्च-गुणवत्ता वाली इमेज बनाएं।
- पॉडकास्ट जनरेशन <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="AI Image Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="पॉडकास्ट जनरेशन" /></p> - AI Resume Builder: अपने मौजूदा रिज़्यूमे को किसी भी जॉब डिस्क्रिप्शन के अनुसार ढालें और ATS को पार करें।
इस तरह के प्रॉम्प्ट आज़माएं:
- इमेज जनरेशन - "मेरे रिज़्यूमे को इस जॉब डिस्क्रिप्शन के अनुसार ढालें ताकि वह ATS पार करे और इंटरव्यू दिलाए।"
- "इस जॉब पोस्टिंग के कीवर्ड्स से मिलान करके मेरे रिज़्यूमे को ATS के लिए ऑप्टिमाइज़ करें।"
- "इस भूमिका के लिए ज़रूरी स्किल्स को उभारने के लिए मेरे रिज़्यूमे के बुलेट पॉइंट फिर से लिखें।"
- "मेरे रिज़्यूमे की तुलना इस जॉब डिस्क्रिप्शन से करें और सुधारने योग्य कमियों की सूची दें।"
- "मेरे रिज़्यूमे और इस जॉब डिस्क्रिप्शन से मेल खाता एक कवर लेटर लिखें।"
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="इमेज जनरेशन" /></p> **सर्च और चैट**
- और भी बहुत कुछ जल्द आ रहा है। - Chat With Your PDFs & Docs: अपनी सभी फ़ाइलों पर सवाल पूछें और इनलाइन उद्धरणों के साथ जवाब पाएं।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Chat With Your PDFs and Docs" /></p>
- AI Search With Citations: अपने पूरे नॉलेज बेस में हाइब्रिड सेमांटिक और कीवर्ड सर्च।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="AI Search With Citations" /></p>
- Collaborative AI Chat: अपनी टीम के साथ रियल टाइम में AI बातचीत पर काम करें।
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeChatGif.gif" alt="Collaborative AI Chat" /></p>
- Comments & Mentions: किसी भी AI संदेश पर टिप्पणी करें और टीम के साथियों को टैग करें।
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeCommentsFlow.gif" alt="Comments and Mentions" /></p>
**कनेक्टर्स और इंटीग्रेशन**
- Connect & Sync Your Tools: Notion, Slack, Google Drive, Gmail, GitHub, Linear और 25+ स्रोतों को एक खोजने योग्य कॉर्पस में सिंक करें।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif" alt="Connect and Sync Your Tools" /></p>
- Chat With Uploaded Files: PDF, Office दस्तावेज़, इमेज और ऑडियो अपलोड करें। तुरंत खोजने योग्य।
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif" alt="Chat With Uploaded Files" /></p>
- Connector Write-Back: एजेंट को परिणाम वापस Notion, Slack, Linear और Drive में पोस्ट करने दें।
इस तरह के प्रॉम्प्ट आज़माएं:
- "इस रिसर्च सारांश को मेरे Notion वर्कस्पेस में पोस्ट करें।"
- "इन मीटिंग एक्शन आइटम्स को हमारे टीम Slack चैनल पर भेजें।"
- "इस बग रिपोर्ट से एक Jira टिकट बनाएं।"
- "इस फ़ीचर अनुरोध से Linear में एक इश्यू खोलें।"
- "इस जनरेट की गई रिपोर्ट को Google Drive में एक डॉक के रूप में सेव करें।"
- Obsidian & Knowledge Base Sync: अपने Obsidian vault और व्यक्तिगत नॉलेज बेस को सिंक रखें।
**ऑटोमेशन**
- Scheduled AI Workflows: किसी एजेंट को शेड्यूल पर चलाएं: रोज़ाना ब्रीफ़, साप्ताहिक डाइजेस्ट, आवर्ती रिपोर्ट।
इस तरह के प्रॉम्प्ट आज़माएं:
- "हर सुबह मेरे नॉलेज बेस में जुड़े नए दस्तावेज़ों का रोज़ाना ब्रीफ़ मुझे ईमेल करें।"
- "हर शुक्रवार मेरे Slack और Gmail से एक साप्ताहिक स्टेटस रिपोर्ट बनाएं।"
- "एक मासिक प्रतिस्पर्धी विश्लेषण रिपोर्ट चलाएं और उसे मेरे वर्कस्पेस में सेव करें।"
- "मेरी GitHub और Linear गतिविधि को एक रोज़ाना standup अपडेट में सारांशित करें।"
- "मैं जिन विषयों को ट्रैक करता हूं उन पर एक आवर्ती साप्ताहिक रिसर्च रिपोर्ट बनाएं।"
- Event-Triggered Automations: जैसे ही कोई दस्तावेज़ किसी फ़ोल्डर में आता है, एजेंट को चलाएं और परिणाम अपने टूल में पोस्ट करें।
इस तरह के प्रॉम्प्ट आज़माएं:
- "जब मेरे Research फ़ोल्डर में कोई PDF आए, तो उद्धरण सहित एक AI सारांश बनाएं।"
- "जब नई मीटिंग नोट्स जुड़ें, तो उन्हें एक्शन आइटम्स के साथ मीटिंग मिनट्स में बदलें।"
- "जब कोई इनवॉइस अपलोड हो, तो विक्रेता, कुल राशि और देय तिथि को एक तालिका में निकालें।"
- "जब मेरे Legal फ़ोल्डर में कोई अनुबंध आए, तो मुख्य शर्तों और नवीनीकरण तिथियों को चिह्नित करें।"
- "जब Candidates में कोई रिज़्यूमे जुड़े, तो उसे जॉब डिस्क्रिप्शन के विरुद्ध स्क्रीन करें।"
- Chat-Built Automations: सरल भाषा में किसी ऑटोमेशन का वर्णन करें और SurfSense उसे आपके लिए बना देगा।
इस तरह के प्रॉम्प्ट आज़माएं:
- "एक AI एजेंट बनाएं जो हर सुबह नई Notion पेजों का सारांश मुझे ईमेल करे।"
- "एक नो-कोड ऑटोमेशन बनाएं जो हर सप्ताह एक रिसर्च डाइजेस्ट Slack पर पोस्ट करे।"
- "एक AI नोट-टेकर सेट करें जो नई मीटिंग नोट्स को मिनट्स में बदल दे।"
- "एक वर्कफ़्लो बनाएं जो मीटिंग नोट्स से एक्शन आइटम्स निकाले और ज़िम्मेदार सौंपे।"
- "मेरे Gmail और Google Drive से एक रोज़ाना ईमेल ब्रीफ़ को ऑटोमेट करें।"
### सेल्फ-होस्टेड ### सेल्फ-होस्टेड
@ -199,6 +270,7 @@ SurfSense एक डेस्कटॉप ऐप भी प्रदान क
| **वीडियो जनरेशन** | Veo 3 के माध्यम से सिनेमैटिक वीडियो ओवरव्यू (केवल Ultra) | उपलब्ध (NotebookLM यहाँ बेहतर है, सक्रिय रूप से सुधार हो रहा है) | | **वीडियो जनरेशन** | Veo 3 के माध्यम से सिनेमैटिक वीडियो ओवरव्यू (केवल Ultra) | उपलब्ध (NotebookLM यहाँ बेहतर है, सक्रिय रूप से सुधार हो रहा है) |
| **प्रेजेंटेशन जनरेशन** | बेहतर दिखने वाली स्लाइड्स लेकिन संपादन योग्य नहीं | संपादन योग्य, स्लाइड आधारित प्रेजेंटेशन बनाएं | | **प्रेजेंटेशन जनरेशन** | बेहतर दिखने वाली स्लाइड्स लेकिन संपादन योग्य नहीं | संपादन योग्य, स्लाइड आधारित प्रेजेंटेशन बनाएं |
| **पॉडकास्ट जनरेशन** | कस्टमाइज़ेबल होस्ट और भाषाओं के साथ ऑडियो ओवरव्यू | कई TTS प्रदाताओं के साथ उपलब्ध (NotebookLM यहाँ बेहतर है, सक्रिय रूप से सुधार हो रहा है) | | **पॉडकास्ट जनरेशन** | कस्टमाइज़ेबल होस्ट और भाषाओं के साथ ऑडियो ओवरव्यू | कई TTS प्रदाताओं के साथ उपलब्ध (NotebookLM यहाँ बेहतर है, सक्रिय रूप से सुधार हो रहा है) |
| **AI ऑटोमेशन और एजेंट** | नहीं | शेड्यूल किए गए AI वर्कफ़्लो, नए दस्तावेज़ों पर इवेंट ट्रिगर, और चैट से बने बिना-कोड ऑटोमेशन, Notion, Slack, Linear और Jira में कनेक्टर राइट-बैक के साथ |
| **डेस्कटॉप ऐप** | नहीं | General Assist, Quick Assist, Screenshot Assist और लोकल फ़ोल्डर सिंक के साथ नेटिव ऐप | | **डेस्कटॉप ऐप** | नहीं | General Assist, Quick Assist, Screenshot Assist और लोकल फ़ोल्डर सिंक के साथ नेटिव ऐप |
| **ब्राउज़र एक्सटेंशन** | नहीं | किसी भी वेबपेज को सहेजने के लिए क्रॉस-ब्राउज़र एक्सटेंशन, प्रमाणीकरण सुरक्षित पेज सहित | | **ब्राउज़र एक्सटेंशन** | नहीं | किसी भी वेबपेज को सहेजने के लिए क्रॉस-ब्राउज़र एक्सटेंशन, प्रमाणीकरण सुरक्षित पेज सहित |

108
README.md
View file

@ -42,6 +42,7 @@ NotebookLM is one of the best and most useful AI platforms out there, but once y
- **25+ External Data Sources** - Add your sources from Google Drive, OneDrive, Dropbox, Notion, and many other external services. - **25+ External Data Sources** - Add your sources from Google Drive, OneDrive, Dropbox, Notion, and many other external services.
- **Real-Time Multiplayer Support** - Work easily with your team members in a shared notebook. - **Real-Time Multiplayer Support** - Work easily with your team members in a shared notebook.
- **AI File Sorting** - Automatically organize your documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic. - **AI File Sorting** - Automatically organize your documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic.
- **AI Automations & Agents** - Run AI agents on a schedule or trigger them the moment a document lands in a folder, then write results back to Notion, Slack, Linear, and Drive. Build no-code automations just by describing them in chat.
- **Desktop App** - Get AI assistance in any application with Quick Assist, General Assist, Screenshot Assist, and local folder sync. - **Desktop App** - Get AI assistance in any application with Quick Assist, General Assist, Screenshot Assist, and local folder sync.
...and more to come. ...and more to come.
@ -77,48 +78,118 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
4. Once everything is indexed, Ask Away (Use Cases): 4. Once everything is indexed, Ask Away (Use Cases):
- Desktop App — General Assist **Desktop App** (native extras on top of everything below, not a separate feature set)
- General Assist: launch SurfSense instantly from any application with a global shortcut.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p>
- Desktop App — Quick Assist - Quick Assist: select text anywhere, then ask AI to explain, rewrite, or act on it.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p>
- Desktop App — Screenshot Assist - Screenshot Assist: capture any region of your screen and ask AI about what's in it.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p>
- Desktop App — Watch Local Folder - Watch Local Folder: auto-sync a local folder to your knowledge base. Great for Obsidian vaults.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p>
- Video Generation **Deliverable Studio**
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="Video Generation" /></p> - AI Report Generator: generate cited research reports and export to PDF, DOCX, HTML, LaTeX, EPUB, ODT, or plain text.
- Basic search and citation <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="AI Report Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="Search and Citation" /></p> - AI Podcast Generator: turn any document or folder into a two-host AI podcast in under 20 seconds.
- Document Mention QNA <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="AI Podcast Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Document Mention QNA" /></p> - AI Presentation & Video Maker: create editable slide decks and narrated video overviews from your sources.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Document Mention QNA" /></p>
- Report Generations and Exports (PDF, DOCX, HTML, LaTeX, EPUB, ODT, Plain Text) <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="AI Presentation and Video Maker" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="Report Generation" /></p> - AI Image Generator: generate high-quality images straight from your chats and documents.
- Podcast Generations <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="AI Image Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="Podcast Generation" /></p> - AI Resume Builder: tailor your existing resume to any job description and beat the ATS.
Try prompts like these:
- Image Generations - "Tailor my resume to this job description so it gets past ATS and lands an interview."
- "Optimize my resume for ATS by matching the keywords in this job posting."
- "Rewrite my resume bullet points to highlight the skills this role is asking for."
- "Compare my resume against this job description and list the gaps to fix."
- "Write a matching cover letter from my resume and this job description."
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="Image Generation" /></p> **Search & Chat**
- And more coming soon. - Chat With Your PDFs & Docs: ask questions across all your files and get answers with inline citations.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Chat With Your PDFs and Docs" /></p>
- AI Search With Citations: hybrid semantic and keyword search across your entire knowledge base.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="AI Search With Citations" /></p>
- Collaborative AI Chat: work on AI conversations with your team in real time.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeChatGif.gif" alt="Collaborative AI Chat" /></p>
- Comments & Mentions: comment and tag teammates on any AI message.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeCommentsFlow.gif" alt="Comments and Mentions" /></p>
**Connectors & Integrations**
- Connect & Sync Your Tools: sync Notion, Slack, Google Drive, Gmail, GitHub, Linear and 25+ sources into one searchable corpus.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif" alt="Connect and Sync Your Tools" /></p>
- Chat With Uploaded Files: drop in PDFs, Office docs, images and audio. Instantly searchable.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif" alt="Chat With Uploaded Files" /></p>
- Connector Write-Back: let the agent post results back to Notion, Slack, Linear and Drive.
Try prompts like these:
- "Post this research summary to my Notion workspace."
- "Send these meeting action items to our team Slack channel."
- "Create a Jira ticket from this bug report."
- "Open a Linear issue from this feature request."
- "Save this generated report to Google Drive as a doc."
- Obsidian & Knowledge Base Sync: keep your Obsidian vault and personal knowledge base in sync.
**Automations**
- Scheduled AI Workflows: run an agent on a schedule: daily briefs, weekly digests, recurring reports.
Try prompts like these:
- "Email me a daily brief of new documents in my knowledge base every morning."
- "Generate a weekly status report from my Slack and Gmail every Friday."
- "Run a monthly competitor analysis report and save it to my workspace."
- "Summarize my GitHub and Linear activity into a daily standup update."
- "Create a recurring weekly research report on the topics I track."
- Event-Triggered Automations: fire an agent the moment a document lands in a folder, then post the result to your tools.
Try prompts like these:
- "When a PDF lands in my Research folder, generate a cited AI summary."
- "When new meeting notes are added, turn them into meeting minutes with action items."
- "When an invoice is uploaded, extract the vendor, total, and due date into a table."
- "When a contract enters my Legal folder, flag key terms and renewal dates."
- "When a resume is added to Candidates, screen it against the job description."
- Chat-Built Automations: describe an automation in plain English and SurfSense builds it for you.
Try prompts like these:
- "Build an AI agent that emails me a summary of new Notion pages each morning."
- "Create a no-code automation that posts a weekly research digest to Slack."
- "Set up an AI note taker that turns new meeting notes into minutes."
- "Make a workflow that extracts action items from meeting notes and assigns owners."
- "Automate a daily email brief from my Gmail and Google Drive."
### Self Hosted ### Self Hosted
@ -201,6 +272,7 @@ All features operate against your chosen search space, so your answers are alway
| **Presentation Generation** | Better looking slides but not editable | Create editable, slide-based presentations | | **Presentation Generation** | Better looking slides but not editable | Create editable, slide-based presentations |
| **Podcast Generation** | Audio Overviews with customizable hosts and languages | Available with multiple TTS providers (NotebookLM is better here, actively improving) | | **Podcast Generation** | Audio Overviews with customizable hosts and languages | Available with multiple TTS providers (NotebookLM is better here, actively improving) |
| **AI File Sorting** | No | LLM-powered auto-categorization into source, date, category, and subcategory folders | | **AI File Sorting** | No | LLM-powered auto-categorization into source, date, category, and subcategory folders |
| **AI Automations & Agents** | No | Scheduled AI workflows, event triggers on new documents, and chat-built no-code automations with connector write-back to Notion, Slack, Linear & Jira |
| **Desktop App** | No | Native app with General Assist, Quick Assist, Screenshot Assist, and local folder sync | | **Desktop App** | No | Native app with General Assist, Quick Assist, Screenshot Assist, and local folder sync |
| **Browser Extension** | No | Cross-browser extension to save any webpage, including auth-protected pages | | **Browser Extension** | No | Cross-browser extension to save any webpage, including auth-protected pages |

View file

@ -41,6 +41,7 @@ O NotebookLM é uma das melhores e mais úteis plataformas de IA disponíveis, m
- **Sem Dependência de Fornecedor** - Configure qualquer modelo LLM, de imagem, TTS e STT. - **Sem Dependência de Fornecedor** - Configure qualquer modelo LLM, de imagem, TTS e STT.
- **25+ Fontes de Dados Externas** - Adicione suas fontes do Google Drive, OneDrive, Dropbox, Notion e muitos outros serviços externos. - **25+ Fontes de Dados Externas** - Adicione suas fontes do Google Drive, OneDrive, Dropbox, Notion e muitos outros serviços externos.
- **Suporte Multiplayer em Tempo Real** - Trabalhe facilmente com os membros da sua equipe em um notebook compartilhado. - **Suporte Multiplayer em Tempo Real** - Trabalhe facilmente com os membros da sua equipe em um notebook compartilhado.
- **Automações e Agentes de IA** - Execute agentes de IA em uma programação ou dispare-os no momento em que um documento chega a uma pasta, e escreva os resultados de volta no Notion, Slack, Linear e Drive. Crie automações sem código apenas descrevendo-as no chat.
- **Aplicativo Desktop** - Obtenha assistência de IA em qualquer aplicativo com Quick Assist, General Assist, Screenshot Assist e sincronização de pastas locais. - **Aplicativo Desktop** - Obtenha assistência de IA em qualquer aplicativo com Quick Assist, General Assist, Screenshot Assist e sincronização de pastas locais.
...e mais por vir. ...e mais por vir.
@ -76,48 +77,118 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
4. Quando tudo estiver indexado, pergunte o que quiser (Casos de uso): 4. Quando tudo estiver indexado, pergunte o que quiser (Casos de uso):
- Aplicativo Desktop — General Assist **Aplicativo Desktop** (extras nativos, além de tudo o que está abaixo, não um conjunto separado)
- General Assist: abra o SurfSense instantaneamente de qualquer aplicativo com um atalho global.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p>
- Aplicativo Desktop — Quick Assist - Quick Assist: selecione um texto em qualquer lugar e peça à IA para explicar, reescrever ou agir sobre ele.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p>
- Aplicativo Desktop — Screenshot Assist - Screenshot Assist: capture qualquer região da tela e pergunte à IA sobre o que está nela.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p>
- Aplicativo Desktop — Watch Local Folder - Watch Local Folder: sincronize automaticamente uma pasta local com sua base de conhecimento. Ótimo para cofres do Obsidian.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p>
- Geração de vídeos **Estúdio de Entregáveis**
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="Geração de Vídeos" /></p> - AI Report Generator: gere relatórios de pesquisa com citações e exporte para PDF, DOCX, HTML, LaTeX, EPUB, ODT ou texto simples.
- Busca básica e citações <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="AI Report Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="Busca e Citação" /></p> - AI Podcast Generator: transforme qualquer documento ou pasta em um podcast de IA com dois apresentadores em menos de 20 segundos.
- QNA com menção de documentos <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="AI Podcast Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="QNA com Menção de Documentos" /></p> - AI Presentation & Video Maker: crie apresentações editáveis e vídeos narrados a partir das suas fontes.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="QNA com Menção de Documentos" /></p>
- Geração de relatórios e exportações (PDF, DOCX, HTML, LaTeX, EPUB, ODT, texto simples) <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="AI Presentation and Video Maker" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="Geração de Relatórios" /></p> - AI Image Generator: gere imagens de alta qualidade diretamente das suas conversas e documentos.
- Geração de podcasts <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="AI Image Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="Geração de Podcasts" /></p> - AI Resume Builder: adapte seu currículo atual a qualquer descrição de vaga e supere o ATS.
Experimente prompts como estes:
- Geração de imagens - "Adapte meu currículo a esta descrição de vaga para passar pelo ATS e conseguir uma entrevista."
- "Otimize meu currículo para o ATS combinando as palavras-chave desta vaga."
- "Reescreva os tópicos do meu currículo para destacar as habilidades que esta vaga exige."
- "Compare meu currículo com esta descrição de vaga e liste as lacunas a corrigir."
- "Escreva uma carta de apresentação combinando com meu currículo e esta descrição de vaga."
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="Geração de Imagens" /></p> **Busca e Chat**
- E mais em breve. - Chat With Your PDFs & Docs: faça perguntas sobre todos os seus arquivos e receba respostas com citações inline.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Chat With Your PDFs and Docs" /></p>
- AI Search With Citations: busca híbrida semântica e por palavra-chave em toda a sua base de conhecimento.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="AI Search With Citations" /></p>
- Collaborative AI Chat: trabalhe em conversas de IA com sua equipe em tempo real.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeChatGif.gif" alt="Collaborative AI Chat" /></p>
- Comments & Mentions: comente e marque colegas em qualquer mensagem de IA.
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeCommentsFlow.gif" alt="Comments and Mentions" /></p>
**Conectores e Integrações**
- Connect & Sync Your Tools: sincronize Notion, Slack, Google Drive, Gmail, GitHub, Linear e mais de 25 fontes em um único acervo pesquisável.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif" alt="Connect and Sync Your Tools" /></p>
- Chat With Uploaded Files: envie PDFs, documentos do Office, imagens e áudio. Pesquisáveis instantaneamente.
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif" alt="Chat With Uploaded Files" /></p>
- Connector Write-Back: deixe o agente publicar os resultados de volta no Notion, Slack, Linear e Drive.
Experimente prompts como estes:
- "Publique este resumo de pesquisa no meu espaço do Notion."
- "Envie estes itens de ação da reunião para o nosso canal do Slack."
- "Crie um ticket no Jira a partir deste relatório de bug."
- "Abra uma issue no Linear a partir desta solicitação de funcionalidade."
- "Salve este relatório gerado no Google Drive como um documento."
- Obsidian & Knowledge Base Sync: mantenha seu cofre do Obsidian e sua base de conhecimento pessoal sincronizados.
**Automações**
- Scheduled AI Workflows: execute um agente em uma programação: resumos diários, boletins semanais, relatórios recorrentes.
Experimente prompts como estes:
- "Envie-me todas as manhãs um resumo diário dos novos documentos na minha base de conhecimento."
- "Gere um relatório de status semanal a partir do meu Slack e Gmail toda sexta-feira."
- "Execute um relatório mensal de análise da concorrência e salve-o no meu espaço de trabalho."
- "Resuma minha atividade no GitHub e Linear em uma atualização diária de standup."
- "Crie um relatório de pesquisa semanal recorrente sobre os temas que acompanho."
- Event-Triggered Automations: dispare um agente no momento em que um documento chega a uma pasta e publique o resultado nas suas ferramentas.
Experimente prompts como estes:
- "Quando um PDF chegar à minha pasta de Pesquisa, gere um resumo com IA e citações."
- "Quando novas notas de reunião forem adicionadas, transforme-as em atas com itens de ação."
- "Quando uma fatura for enviada, extraia o fornecedor, o total e a data de vencimento em uma tabela."
- "Quando um contrato entrar na minha pasta Jurídica, sinalize os termos-chave e as datas de renovação."
- "Quando um currículo for adicionado a Candidatos, avalie-o em relação à descrição da vaga."
- Chat-Built Automations: descreva uma automação em linguagem simples e o SurfSense a cria para você.
Experimente prompts como estes:
- "Crie um agente de IA que me envie todas as manhãs um resumo das novas páginas do Notion."
- "Crie uma automação sem código que publique um resumo de pesquisa semanal no Slack."
- "Configure um anotador com IA que transforme as novas notas de reunião em atas."
- "Crie um fluxo que extraia os itens de ação das notas de reunião e atribua responsáveis."
- "Automatize um resumo diário por e-mail a partir do meu Gmail e Google Drive."
### Auto-Hospedado ### Auto-Hospedado
@ -199,6 +270,7 @@ Todos os recursos operam no espaço de busca escolhido, para que suas respostas
| **Geração de Vídeos** | Visões gerais cinemáticas via Veo 3 (apenas Ultra) | Disponível (NotebookLM é melhor aqui, melhorando ativamente) | | **Geração de Vídeos** | Visões gerais cinemáticas via Veo 3 (apenas Ultra) | Disponível (NotebookLM é melhor aqui, melhorando ativamente) |
| **Geração de Apresentações** | Slides mais bonitos mas não editáveis | Cria apresentações editáveis baseadas em slides | | **Geração de Apresentações** | Slides mais bonitos mas não editáveis | Cria apresentações editáveis baseadas em slides |
| **Geração de Podcasts** | Visões gerais em áudio com hosts e idiomas personalizáveis | Disponível com múltiplos provedores TTS (NotebookLM é melhor aqui, melhorando ativamente) | | **Geração de Podcasts** | Visões gerais em áudio com hosts e idiomas personalizáveis | Disponível com múltiplos provedores TTS (NotebookLM é melhor aqui, melhorando ativamente) |
| **Automações e Agentes de IA** | Não | Fluxos de trabalho de IA agendados, gatilhos por eventos em novos documentos e automações sem código criadas por chat com escrita de volta no Notion, Slack, Linear e Jira |
| **Aplicativo Desktop** | Não | Aplicativo nativo com General Assist, Quick Assist, Screenshot Assist e sincronização de pastas locais | | **Aplicativo Desktop** | Não | Aplicativo nativo com General Assist, Quick Assist, Screenshot Assist e sincronização de pastas locais |
| **Extensão de Navegador** | Não | Extensão multi-navegador para salvar qualquer página web, incluindo páginas protegidas por autenticação | | **Extensão de Navegador** | Não | Extensão multi-navegador para salvar qualquer página web, incluindo páginas protegidas por autenticação |

View file

@ -41,6 +41,7 @@ NotebookLM 是目前最好、最实用的 AI 平台之一,但当你开始经
- **无供应商锁定** - 配置任何 LLM、图像、TTS 和 STT 模型。 - **无供应商锁定** - 配置任何 LLM、图像、TTS 和 STT 模型。
- **25+ 外部数据源** - 从 Google Drive、OneDrive、Dropbox、Notion 和许多其他外部服务添加你的来源。 - **25+ 外部数据源** - 从 Google Drive、OneDrive、Dropbox、Notion 和许多其他外部服务添加你的来源。
- **实时多人协作支持** - 在共享笔记本中轻松与团队成员协作。 - **实时多人协作支持** - 在共享笔记本中轻松与团队成员协作。
- **AI 自动化与智能体** - 按计划运行 AI 智能体,或在文档进入文件夹的那一刻触发它们,然后将结果回写到 Notion、Slack、Linear 和 Drive。只需在聊天中描述即可创建无代码自动化。
- **桌面应用** - 通过 Quick Assist、General Assist、Screenshot Assist 和本地文件夹同步在任何应用程序中获得 AI 助手。 - **桌面应用** - 通过 Quick Assist、General Assist、Screenshot Assist 和本地文件夹同步在任何应用程序中获得 AI 助手。
...更多功能即将推出。 ...更多功能即将推出。
@ -76,48 +77,118 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
4. 一切索引完成后,尽管提问(使用场景): 4. 一切索引完成后,尽管提问(使用场景):
- 桌面应用 — General Assist **桌面应用**(在以下所有功能之外的原生附加功能,并非独立的功能集)
- General Assist通过全局快捷键从任意应用中即刻打开 SurfSense。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/general_assist.gif" alt="General Assist" /></p>
- 桌面应用 — Quick Assist - Quick Assist:在任意位置选中文本,让 AI 解释、改写或对其执行操作。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/quick_assist.gif" alt="Quick Assist" /></p>
- 桌面应用 — Screenshot Assist - Screenshot Assist:截取屏幕上任意区域,并就其中内容向 AI 提问。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/screenshot_assist.gif" alt="Screenshot Assist" /></p>
- 桌面应用 — Watch Local Folder - Watch Local Folder:将本地文件夹自动同步到你的知识库。非常适合 Obsidian 库。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p> <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/folder_watch.gif" alt="Watch Local Folder" /></p>
- 视频生成 **成果工作室**
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="视频生成" /></p> - AI Report Generator生成带引用的研究报告并导出为 PDF、DOCX、HTML、LaTeX、EPUB、ODT 或纯文本。
- 基本搜索和引用 <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="AI Report Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="搜索和引用" /></p> - AI Podcast Generator在 20 秒内将任意文档或文件夹转换为双主持人 AI 播客。
- 文档提及问答 <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="AI Podcast Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="文档提及问答" /></p> - AI Presentation & Video Maker根据你的资料创建可编辑的幻灯片和带旁白的视频概览。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="文档提及问答" /></p>
- 报告生成和导出PDF、DOCX、HTML、LaTeX、EPUB、ODT、纯文本 <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/video_gen_gif.gif" alt="AI Presentation and Video Maker" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif" alt="报告生成" /></p> - AI Image Generator直接从你的聊天和文档生成高质量图像。
- 播客生成 <p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="AI Image Generator" /></p>
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif" alt="播客生成" /></p> - AI Resume Builder根据任意职位描述定制你现有的简历顺利通过 ATS。
可以试试这样的提示:
- 图像生成 - “根据这份职位描述定制我的简历,让它通过 ATS 并赢得面试。”
- “匹配这份招聘启事中的关键词,为 ATS 优化我的简历。”
- “重写我的简历要点,突出这个岗位所需要的技能。”
- “将我的简历与这份职位描述对比,列出需要改进的差距。”
- “根据我的简历和这份职位描述,写一封相匹配的求职信。”
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif" alt="图像生成" /></p> **搜索与聊天**
- 更多功能即将推出。 - Chat With Your PDFs & Docs跨所有文件提问并获得带内联引用的答案。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif" alt="Chat With Your PDFs and Docs" /></p>
- AI Search With Citations在整个知识库中进行语义与关键词的混合搜索。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif" alt="AI Search With Citations" /></p>
- Collaborative AI Chat与团队实时协作处理 AI 对话。
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeChatGif.gif" alt="Collaborative AI Chat" /></p>
- Comments & Mentions在任意 AI 消息上评论并 @ 你的队友。
<p align="center"><img src="surfsense_web/public/homepage/hero_realtime/RealTimeCommentsFlow.gif" alt="Comments and Mentions" /></p>
**连接器与集成**
- Connect & Sync Your Tools将 Notion、Slack、Google Drive、Gmail、GitHub、Linear 等 25+ 数据源同步为一个可搜索的语料库。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif" alt="Connect and Sync Your Tools" /></p>
- Chat With Uploaded Files上传 PDF、Office 文档、图像和音频。即刻可搜索。
<p align="center"><img src="surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif" alt="Chat With Uploaded Files" /></p>
- Connector Write-Back让智能体将结果回写到 Notion、Slack、Linear 和 Drive。
可以试试这样的提示:
- “把这份研究摘要发布到我的 Notion 工作区。”
- “把这些会议行动项发送到我们的团队 Slack 频道。”
- “根据这份缺陷报告创建一个 Jira 工单。”
- “根据这个功能需求在 Linear 中创建一个 issue。”
- “把这份生成的报告作为文档保存到 Google Drive。”
- Obsidian & Knowledge Base Sync让你的 Obsidian 库与个人知识库保持同步。
**自动化**
- Scheduled AI Workflows按计划运行智能体每日简报、每周摘要、周期性报告。
可以试试这样的提示:
- “每天早上把我知识库中新增文档的每日简报发邮件给我。”
- “每周五根据我的 Slack 和 Gmail 生成一份每周状态报告。”
- “每月运行一次竞争对手分析报告并保存到我的工作区。”
- “把我的 GitHub 和 Linear 活动汇总成一份每日站会更新。”
- “针对我关注的主题创建一份周期性的每周研究报告。”
- Event-Triggered Automations在文档进入文件夹的那一刻触发智能体并将结果发布到你的工具中。
可以试试这样的提示:
- “当一个 PDF 进入我的 Research 文件夹时,生成一份带引用的 AI 摘要。”
- “当新增会议记录时,把它整理成带行动项的会议纪要。”
- “当上传发票时,把供应商、总额和到期日提取到一张表格中。”
- “当一份合同进入我的 Legal 文件夹时,标记关键条款和续约日期。”
- “当一份简历加入 Candidates 时,根据职位描述对其进行筛选。”
- Chat-Built Automations用通俗的语言描述一个自动化SurfSense 就会为你构建它。
可以试试这样的提示:
- “创建一个 AI 智能体,每天早上把新增 Notion 页面的摘要发邮件给我。”
- “创建一个无代码自动化,每周把研究摘要发布到 Slack。”
- “设置一个 AI 笔记助手,把新增会议记录整理成纪要。”
- “创建一个工作流,从会议记录中提取行动项并指派负责人。”
- “自动化一份来自我的 Gmail 和 Google Drive 的每日邮件简报。”
### 自托管 ### 自托管
@ -199,6 +270,7 @@ SurfSense 还提供桌面应用,将 AI 助手带到您计算机上的每个应
| **视频生成** | 通过 Veo 3 的电影级视频概览(仅 Ultra | 可用NotebookLM 在此方面更好,正在积极改进) | | **视频生成** | 通过 Veo 3 的电影级视频概览(仅 Ultra | 可用NotebookLM 在此方面更好,正在积极改进) |
| **演示文稿生成** | 更美观的幻灯片但不可编辑 | 创建可编辑的幻灯片式演示文稿 | | **演示文稿生成** | 更美观的幻灯片但不可编辑 | 创建可编辑的幻灯片式演示文稿 |
| **播客生成** | 可自定义主持人和语言的音频概览 | 可用,支持多种 TTS 提供商NotebookLM 在此方面更好,正在积极改进) | | **播客生成** | 可自定义主持人和语言的音频概览 | 可用,支持多种 TTS 提供商NotebookLM 在此方面更好,正在积极改进) |
| **AI 自动化与智能体** | 否 | 定时 AI 工作流、新文档的事件触发,以及通过聊天构建的无代码自动化,支持回写到 Notion、Slack、Linear 和 Jira |
| **桌面应用** | 否 | 原生应用,包含 General Assist、Quick Assist、Screenshot Assist 和本地文件夹同步 | | **桌面应用** | 否 | 原生应用,包含 General Assist、Quick Assist、Screenshot Assist 和本地文件夹同步 |
| **浏览器扩展** | 否 | 跨浏览器扩展,保存任何网页,包括需要身份验证的页面 | | **浏览器扩展** | 否 | 跨浏览器扩展,保存任何网页,包括需要身份验证的页面 |

View file

@ -1 +1 @@
0.0.25 0.0.26

View file

@ -0,0 +1,168 @@
"""Minimal anonymous / free-chat agent.
The no-login chat experience must stay dead simple: the user asks a question
and the model answers, optionally using ``web_search`` and an optionally
uploaded **read-only** document. We deliberately bypass the full SurfSense deep
agent stack (filesystem, file-intent, knowledge-base persistence, subagents,
skills, memory) because those middlewares stage or persist "documents" that an
anonymous session can never see again -- which produced phantom
"I saved it to a file" answers for free users.
For any other SurfSense capability the model is instructed (via the system
prompt built here) to tell the user to create a free account instead of
pretending to perform the action.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from deepagents.backends import StateBackend
from langchain.agents import create_agent
from langchain.agents.middleware import (
ModelCallLimitMiddleware,
ToolCallLimitMiddleware,
)
from langchain_core.language_models import BaseChatModel
from langgraph.types import Checkpointer
from app.agents.new_chat.context import SurfSenseContextSchema
from app.agents.new_chat.middleware import (
RetryAfterMiddleware,
create_surfsense_compaction_middleware,
)
from app.agents.new_chat.tools.web_search import create_web_search_tool
# Cap how much of an uploaded document we inline into the system prompt. The
# upload endpoint allows files up to several MB, but the doc is re-sent on
# every turn and counts against the anonymous token quota, so we bound it.
_MAX_DOC_CHARS = 50_000
def build_anonymous_system_prompt(anon_doc: dict[str, Any] | None = None) -> str:
"""Build the system prompt for the minimal anonymous chat agent.
The prompt keeps the assistant focused on plain Q/A + web search, inlines
any uploaded document as read-only context, and redirects every other
SurfSense feature to account registration.
"""
today = datetime.now(UTC).strftime("%A, %B %d, %Y")
doc_section = ""
if anon_doc:
title = str(anon_doc.get("title") or "uploaded_document")
content = str(anon_doc.get("content") or "")
truncated = content[:_MAX_DOC_CHARS]
truncation_note = ""
if len(content) > _MAX_DOC_CHARS:
truncation_note = (
"\n\n[Note: the document was truncated because it is large; "
"only the beginning is shown.]"
)
doc_section = (
"\n\n## Uploaded document (read-only)\n"
f'The user uploaded a document named "{title}". Its contents are '
"provided below for reference only. You may read it and answer "
"questions about it, but you cannot modify, save, or store it.\n\n"
f'<uploaded_document title="{title}">\n'
f"{truncated}{truncation_note}\n"
"</uploaded_document>"
)
return (
"You are SurfSense's free AI assistant, available to everyone without "
"login.\n\n"
f"Today's date is {today}.\n\n"
"## How to help\n"
"- Answer the user's questions directly and conversationally. You are "
"a straightforward question-and-answer assistant.\n"
"- When a question needs current, real-time, or factual information "
"from the internet (news, prices, weather, recent events, live data), "
"use the `web_search` tool. Otherwise, answer directly from your own "
"knowledge.\n"
"- Be concise, accurate, and helpful. Use Markdown formatting when it "
"improves readability."
f"{doc_section}\n\n"
"## What is not available here\n"
"This is the free, no-login experience. You CANNOT save files or "
"notes, generate reports, podcasts, resumes, presentations, or images, "
"search or build a knowledge base, connect to apps (Gmail, Google "
"Drive, Notion, Slack, Calendar, Discord, and similar), set up "
"automations, or remember anything across sessions.\n\n"
"If the user asks for any of these, do NOT pretend to do them and "
"never claim you saved, created, or stored anything. Instead, briefly "
"let them know the feature requires a free SurfSense account and "
"invite them to create one at https://www.surfsense.com. Then offer to "
"help with what you can do here (answering questions and searching the "
"web)."
)
async def create_anonymous_chat_agent(
*,
llm: BaseChatModel,
checkpointer: Checkpointer,
anon_session_id: str | None = None,
anon_doc: dict[str, Any] | None = None,
enable_web_search: bool = True,
):
"""Create a minimal Q/A agent for anonymous / free chat.
Unlike :func:`create_surfsense_deep_agent`, this agent has no filesystem,
file-intent, knowledge-base persistence, subagent, skills, or memory
middleware. Its only tool is ``web_search`` (when ``enable_web_search`` is
True), and any uploaded document is injected into the system prompt as
read-only context.
Args:
llm: The chat model to use (already built by the caller).
checkpointer: LangGraph checkpointer for the ephemeral anon thread.
anon_session_id: Anonymous session id (used only for telemetry/metadata).
anon_doc: Optional ``{"title", "content"}`` for an uploaded document.
enable_web_search: When False, the agent runs as a pure LLM with no
tools (used when the user toggles web search off).
"""
tools = (
[create_web_search_tool(search_space_id=None, available_connectors=None)]
if enable_web_search
else []
)
# Reliability-only middleware. Nothing here touches the database or
# filesystem: call limits guard against loops, compaction summarises long
# histories into in-graph state, and retry handles provider rate limits.
middleware: list[Any] = [
ModelCallLimitMiddleware(thread_limit=120, run_limit=80, exit_behavior="end"),
]
if tools:
middleware.append(
ToolCallLimitMiddleware(
thread_limit=300, run_limit=80, exit_behavior="continue"
)
)
middleware.append(create_surfsense_compaction_middleware(llm, StateBackend))
middleware.append(RetryAfterMiddleware(max_retries=3))
system_prompt = build_anonymous_system_prompt(anon_doc)
agent = create_agent(
llm,
system_prompt=system_prompt,
tools=tools,
middleware=middleware,
context_schema=SurfSenseContextSchema,
checkpointer=checkpointer,
)
return agent.with_config(
{
"recursion_limit": 40,
"metadata": {
"ls_integration": "surfsense_anonymous_chat",
"anon_session_id": anon_session_id,
},
}
)
__all__ = ["build_anonymous_system_prompt", "create_anonymous_chat_agent"]

View file

@ -65,8 +65,7 @@ def _match_condition(condition: Any, actual: Any) -> bool:
return False return False
if isinstance(condition, dict): if isinstance(condition, dict):
return all( return all(
_apply_operator(op, operand, actual) _apply_operator(op, operand, actual) for op, operand in condition.items()
for op, operand in condition.items()
) )
return actual == condition return actual == condition

View file

@ -41,9 +41,7 @@ async def _select_and_start(event_dict: dict[str, Any]) -> None:
await _start_one(session, trigger=trigger, event=event) await _start_one(session, trigger=trigger, event=event)
async def _eligible( async def _eligible(session: AsyncSession, *, event: Event) -> list[AutomationTrigger]:
session: AsyncSession, *, event: Event
) -> list[AutomationTrigger]:
"""Enabled ``event`` triggers for this event type whose filter matches.""" """Enabled ``event`` triggers for this event type whose filter matches."""
stmt = select(AutomationTrigger).where( stmt = select(AutomationTrigger).where(
AutomationTrigger.type == TriggerType.EVENT, AutomationTrigger.type == TriggerType.EVENT,

View file

@ -351,10 +351,9 @@ async def stream_anonymous_chat(
async def _generate(): async def _generate():
from langchain_core.messages import AIMessage, HumanMessage from langchain_core.messages import AIMessage, HumanMessage
from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent from app.agents.new_chat.anonymous_agent import create_anonymous_chat_agent
from app.agents.new_chat.checkpointer import get_checkpointer from app.agents.new_chat.checkpointer import get_checkpointer
from app.db import shielded_async_session from app.db import shielded_async_session
from app.services.connector_service import ConnectorService
from app.services.new_streaming_service import VercelStreamingService from app.services.new_streaming_service import VercelStreamingService
from app.services.token_tracking_service import start_turn from app.services.token_tracking_service import start_turn
from app.tasks.chat.stream_new_chat import StreamResult, _stream_agent_events from app.tasks.chat.stream_new_chat import StreamResult, _stream_agent_events
@ -363,24 +362,23 @@ async def stream_anonymous_chat(
streaming_service = VercelStreamingService() streaming_service = VercelStreamingService()
try: try:
async with shielded_async_session() as session: async with shielded_async_session():
connector_service = ConnectorService(session, search_space_id=None)
checkpointer = await get_checkpointer() checkpointer = await get_checkpointer()
anon_thread_id = f"anon-{session_id}-{request_id}" anon_thread_id = f"anon-{session_id}-{request_id}"
agent = await create_surfsense_deep_agent( # Load the optional uploaded document as read-only context.
anon_doc = await _load_anon_document(session_id)
# Minimal Q/A agent: web_search only (when enabled), no
# filesystem / persistence / subagents. The uploaded document
# is injected into the system prompt as read-only context.
agent = await create_anonymous_chat_agent(
llm=llm, llm=llm,
search_space_id=0,
db_session=session,
connector_service=connector_service,
checkpointer=checkpointer, checkpointer=checkpointer,
user_id=None,
thread_id=None,
agent_config=agent_config,
enabled_tools=list(enabled_for_agent),
disabled_tools=None,
anon_session_id=session_id, anon_session_id=session_id,
anon_doc=anon_doc,
enable_web_search="web_search" in enabled_for_agent,
) )
langchain_messages = [] langchain_messages = []
@ -396,7 +394,6 @@ async def stream_anonymous_chat(
input_state = { input_state = {
"messages": langchain_messages, "messages": langchain_messages,
"search_space_id": 0,
} }
langgraph_config = { langgraph_config = {
@ -500,6 +497,38 @@ ANON_ALLOWED_EXTENSIONS = PLAINTEXT_EXTENSIONS | DIRECT_CONVERT_EXTENSIONS
ANON_DOC_REDIS_PREFIX = "anon:doc:" ANON_DOC_REDIS_PREFIX = "anon:doc:"
async def _load_anon_document(session_id: str) -> dict[str, Any] | None:
"""Read the anonymous session's uploaded document from Redis.
Returns ``{"title", "content"}`` for read-only injection into the agent's
system prompt, or ``None`` when nothing was uploaded for this session.
"""
import json as _json
import redis.asyncio as aioredis
redis_client = aioredis.from_url(config.REDIS_APP_URL, decode_responses=True)
redis_key = f"{ANON_DOC_REDIS_PREFIX}{session_id}"
try:
data = await redis_client.get(redis_key)
if not data:
return None
payload = _json.loads(data)
except Exception as exc: # pragma: no cover - defensive
logger.warning("Failed to load anonymous document from Redis: %s", exc)
return None
finally:
await redis_client.aclose()
content = str(payload.get("content") or "")
if not content:
return None
return {
"title": str(payload.get("filename") or "uploaded_document"),
"content": content,
}
class AnonDocResponse(BaseModel): class AnonDocResponse(BaseModel):
filename: str filename: str
size_bytes: int size_bytes: int

View file

@ -79,9 +79,11 @@ def _after_commit(session: Session) -> None:
] ]
for task in tasks: for task in tasks:
task.add_done_callback( task.add_done_callback(
lambda t: logger.error("event publish failed: %s", t.exception()) lambda t: (
if not t.cancelled() and t.exception() logger.error("event publish failed: %s", t.exception())
else None if not t.cancelled() and t.exception()
else None
)
) )

View file

@ -1,6 +1,6 @@
[project] [project]
name = "surf-new-backend" name = "surf-new-backend"
version = "0.0.25" version = "0.0.26"
description = "SurfSense Backend" description = "SurfSense Backend"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [

View file

@ -24,6 +24,7 @@ def _event_type(type_: str = "test.thing") -> EventType:
def test_register_then_get_returns_the_event_type(isolated_event_catalog: None) -> None: def test_register_then_get_returns_the_event_type(isolated_event_catalog: None) -> None:
from app.event_bus.catalog import catalog from app.event_bus.catalog import catalog
catalog.register(_event_type()) catalog.register(_event_type())
assert catalog.get("test.thing") is not None assert catalog.get("test.thing") is not None
@ -32,12 +33,14 @@ def test_register_then_get_returns_the_event_type(isolated_event_catalog: None)
def test_get_unknown_type_returns_none(isolated_event_catalog: None) -> None: def test_get_unknown_type_returns_none(isolated_event_catalog: None) -> None:
from app.event_bus.catalog import catalog from app.event_bus.catalog import catalog
assert catalog.get("does.not.exist") is None assert catalog.get("does.not.exist") is None
def test_register_duplicate_type_raises(isolated_event_catalog: None) -> None: def test_register_duplicate_type_raises(isolated_event_catalog: None) -> None:
"""A type is a contract; registering it twice is a bug, not an override.""" """A type is a contract; registering it twice is a bug, not an override."""
from app.event_bus.catalog import catalog from app.event_bus.catalog import catalog
catalog.register(_event_type()) catalog.register(_event_type())
with pytest.raises(ValueError, match="already registered"): with pytest.raises(ValueError, match="already registered"):
@ -47,6 +50,7 @@ def test_register_duplicate_type_raises(isolated_event_catalog: None) -> None:
def test_all_is_a_defensive_snapshot(isolated_event_catalog: None) -> None: def test_all_is_a_defensive_snapshot(isolated_event_catalog: None) -> None:
"""Mutating the returned dict must not corrupt the registry.""" """Mutating the returned dict must not corrupt the registry."""
from app.event_bus.catalog import catalog from app.event_bus.catalog import catalog
catalog.register(_event_type()) catalog.register(_event_type())
snapshot = catalog.all() snapshot = catalog.all()

8503
surfsense_backend/uv.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "surfsense_browser_extension", "name": "surfsense_browser_extension",
"displayName": "Surfsense Browser Extension", "displayName": "Surfsense Browser Extension",
"version": "0.0.25", "version": "0.0.26",
"description": "Extension to collect Browsing History for SurfSense.", "description": "Extension to collect Browsing History for SurfSense.",
"author": "https://github.com/MODSetter", "author": "https://github.com/MODSetter",
"engines": { "engines": {

View file

@ -1,7 +1,7 @@
{ {
"name": "surfsense-desktop", "name": "surfsense-desktop",
"productName": "SurfSense", "productName": "SurfSense",
"version": "0.0.25", "version": "0.0.26",
"description": "SurfSense Desktop App", "description": "SurfSense Desktop App",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {

View file

@ -3,9 +3,9 @@ import PricingBasic from "@/components/pricing/pricing-section";
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav"; import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Pricing | SurfSense - Free AI Search Plans", title: "Pricing | SurfSense - Free AI Workspace, Automations & Agents",
description: description:
"Explore SurfSense plans and pricing. Start free with 500 pages & $5 in premium credits. Use ChatGPT, Claude AI, and premium AI models. Pay as you go at provider cost.", "Explore SurfSense plans and pricing. Start free with 500 pages & $5 in premium credits. Run AI automations and agents, use ChatGPT, Claude AI, and premium AI models, and pay as you go at provider cost.",
alternates: { alternates: {
canonical: "https://www.surfsense.com/pricing", canonical: "https://www.surfsense.com/pricing",
}, },

View file

@ -0,0 +1,76 @@
---
title: "SurfSense v0.0.26 - AI Automations: Build, Schedule & Event-Trigger AI Agents From Chat"
description: "SurfSense v0.0.26 introduces open source AI automations across your connectors: describe a workflow in plain English and SurfSense builds it, run AI agents on a schedule, or trigger them when a document lands in a folder, working across Notion, Slack, Google Drive, Gmail, GitHub, Linear, Jira and Confluence. Plus connector @-mentions in chat and a faster anonymous chat experience."
date: "2026-05-31"
tags: ["Automations", "AI Agents", "Workflow Automation", "Scheduled Workflows", "Event Triggers", "Connectors", "Notion", "Slack", "Open Source"]
version: "0.0.26"
---
## What's New in v0.0.26
v0.0.26 is our biggest workflow release yet: **AI automations** land in SurfSense. If you've been looking for **open source AI automation**, a **self-hosted AI agent** that runs on a schedule, or **document workflow automation** that reacts the instant a file shows up, this release is for you. Describe what you want in plain English, let SurfSense draft the automation, approve it, and your agent runs on its own: on a **cron schedule** or triggered by real events in your knowledge base. Best of all, automations work **across your connectors**, so one workflow can search and act on **Notion**, **Slack**, **Google Drive**, **Gmail**, **GitHub**, **Linear**, **Jira** and **Confluence**. This release also brings connector **@-mentions** into chat, a faster anonymous chat experience, and a redesigned homepage.
### AI Automations
Turn one-off prompts into repeatable, hands-off **AI agent workflows**.
- **Build Automations From Chat**: Describe an automation in plain English and SurfSense drafts it for you. Review the generated workflow in a human-in-the-loop approval card, tweak it, and save. No config files, no code.
- **Scheduled AI Workflows**: Run an agent on a **cron schedule** for daily briefs, weekly digests, and recurring reports. SurfSense computes the next run time and fires it automatically in the background.
- **Event-Triggered Automations**: Kick off an agent the moment a document enters a folder. SurfSense's new in-process event bus watches your knowledge base and launches the right automation as soon as the event happens.
- **Agent Task Action**: Every automation runs a full multi-agent chat turn, so your scheduled and event-driven runs have the same reasoning, search, and tool access as a live chat, with an auto-approve loop so they finish unattended.
- **Automations Across Your Connectors**: Because each automation runs a full agent turn, your scheduled and event-triggered workflows can search and act across all 25+ connected sources, including **Notion**, **Slack**, **Google Drive**, **Gmail**, **GitHub**, **Linear**, **Jira**, **Confluence**, and your **Obsidian** vault.
- **Automations Dashboard**: A dedicated list view shows every automation with its status, plus one-click pause, resume, and delete. Find it in the sidebar under Inbox.
- **Detail & Run History**: Open any automation to inspect its definition, manage triggers inline, and browse a complete run history with inputs, outputs, and clear error reporting.
- **Power-User JSON Mode**: Create or edit an automation directly as raw JSON with a unified JSON viewer and editor for full control over the workflow definition.
- **Role-Based Access**: Automations are governed by SurfSense's RBAC system, with a dedicated permissions family so teams control exactly who can create, run, and manage them.
### Connector Mentions in Chat
- **@-Mention Your Connectors**: Mention connectors directly in the composer to scope a question to a specific source like **Notion**, **Slack**, **Google Drive**, **Gmail**, **GitHub**, **Linear**, **Jira**, or **Confluence**, alongside existing document mentions.
- **Recent Mentions**: SurfSense now remembers your recent mentions so the sources you use most are always one keystroke away.
- **Smoother Mention Picker**: A refreshed mention picker with loading skeletons, clearer connector definitions, and a better inline editing experience.
### Faster, Friendlier Chat
- **Anonymous Chat, Reworked**: The free, no-login chat experience has been rebuilt for a faster first impression and a cleaner anonymous-to-account path.
- **Better Long-Running Turns**: Improved task management and timeout handling in multi-agent chat keep complex, tool-heavy conversations reliable.
- **Leaner Toolset**: Retired the legacy in-product docs search tool to keep agent reasoning focused and fast.
### Homepage & Marketing
- **Redesigned Use-Case Showcase**: The homepage now groups demos into clear categories (Desktop App, Deliverable Studio, Search & Chat, Connectors & Integrations, and Automations) so visitors immediately see what SurfSense can do.
- **Desktop App, Front and Center**: The desktop experience is highlighted as a set of native extras on top of everything SurfSense already does, not a separate product.
<Accordion type="multiple" className="w-full not-prose">
<AccordionItem value="item-1">
<AccordionTrigger>Bug Fixes</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<ul className="list-disc space-y-2 pl-4">
<li>Bulk-moving documents now uses ORM objects so folder events fire correctly and trigger automations</li>
<li>Automation enum columns now persist Postgres enum values instead of names</li>
<li>Automation agent tasks use an in-memory checkpointer to avoid Celery pool timeouts</li>
<li>The API client now handles 204 No Content responses without errors</li>
<li>The model role manager now stays in sync when preferences are updated</li>
<li>The JSON editor now coerces numeric strings to numbers on edit</li>
</ul>
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Under the Hood</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 text-balance">
<ul className="list-disc space-y-2 pl-4">
<li>New in-process domain event bus with an event catalog and a <code>document.entered_folder</code> event</li>
<li>SQLAlchemy session hooks publish folder events automatically, registered at app startup</li>
<li>Cron schedule triggers backed by croniter and a Celery beat tick task</li>
<li>Sandboxed template engine with an allowlisted filter and test set for safe workflow templating</li>
<li>Automations reorganized into a vertical-slice architecture (actions and triggers grouped by domain)</li>
<li>Extensive new test coverage locking automation schemas, dispatch, runtime, triggers, and templating</li>
<li>Model eligibility checks when creating automations, so only valid models are selectable</li>
</ul>
</AccordionContent>
</AccordionItem>
</Accordion>
v0.0.26 turns SurfSense from a place you ask questions into a place that does the work for you. Whether you want **scheduled AI workflows**, **event-driven document automation**, or a **self-hosted, open source AI agent** you fully control, this release lets you build it from a single sentence.
SurfSense connects all your knowledge sources in one place.

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { Bell, ExternalLink, Info, type LucideIcon, Rocket, Wrench, Zap } from "lucide-react"; import { Bell, ExternalLink, Info, type LucideIcon, Rocket, Wrench, Zap } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -49,7 +50,18 @@ export function AnnouncementCard({ announcement }: { announcement: AnnouncementW
const Icon = config.icon; const Icon = config.icon;
return ( return (
<Card className="group relative transition-all duration-200 hover:shadow-md"> <Card className="group relative overflow-hidden transition-all duration-200 hover:shadow-md">
{announcement.image && (
<div className="relative aspect-video w-full overflow-hidden border-b bg-muted">
<Image
src={announcement.image.src}
alt={announcement.image.alt}
fill
sizes="(max-width: 768px) 95vw, 600px"
className="object-cover"
/>
</div>
)}
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 min-w-0"> <div className="flex items-start gap-3 min-w-0">

View file

@ -0,0 +1,101 @@
"use client";
import { ExternalLink } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
import { useAnnouncements } from "@/hooks/use-announcements";
/**
* Proactively shows important "spotlight" announcements in a blocking dialog.
*
* Behaviour:
* - On load, the first active, audience-matched, unread spotlight announcement
* is shown automatically.
* - The user must explicitly acknowledge it ("Got it" or the CTA link), which
* marks it as read so it never shows again.
* - Closing via the X / Escape / outside-click only hides it for the current
* session; it reappears on the next load until the user marks it as seen.
*/
export function AnnouncementSpotlight() {
const { announcements, markRead } = useAnnouncements();
const [sessionDismissed, setSessionDismissed] = useState<Set<string>>(() => new Set());
const [ready, setReady] = useState(false);
// Short delay so the spotlight doesn't flash during initial hydration/layout.
useEffect(() => {
const timer = setTimeout(() => setReady(true), 800);
return () => clearTimeout(timer);
}, []);
const current = useMemo(
() =>
announcements.find(
(a) => a.spotlight && a.isImportant && !a.isRead && !sessionDismissed.has(a.id)
) ?? null,
[announcements, sessionDismissed]
);
if (!current) return null;
const handleAcknowledge = () => {
markRead(current.id);
};
const handleOpenChange = (next: boolean) => {
if (!next) {
setSessionDismissed((prev) => {
const updated = new Set(prev);
updated.add(current.id);
return updated;
});
}
};
return (
<Dialog open={ready} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md gap-0 overflow-hidden p-0">
{current.image && (
<div className="relative aspect-video w-full border-b bg-muted">
<Image
src={current.image.src}
alt={current.image.alt}
fill
sizes="(max-width: 768px) 95vw, 448px"
className="object-cover"
priority
/>
</div>
)}
<div className="flex flex-col gap-3 p-6">
<DialogTitle className="text-xl">{current.title}</DialogTitle>
<DialogDescription className="text-sm leading-relaxed text-muted-foreground">
{current.description}
</DialogDescription>
<DialogFooter className="mt-2">
{current.link && (
<Button variant="outline" asChild className="gap-1.5" onClick={handleAcknowledge}>
<Link
href={current.link.url}
target={current.link.url.startsWith("http") ? "_blank" : undefined}
>
{current.link.label}
<ExternalLink className="h-3.5 w-3.5" />
</Link>
</Button>
)}
<Button onClick={handleAcknowledge}>Got it</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -70,8 +70,10 @@ export function AnnouncementToastProvider() {
const outerTimer = setTimeout(() => { const outerTimer = setTimeout(() => {
const authed = isAuthenticated(); const authed = isAuthenticated();
const active = getActiveAnnouncements(announcements, authed); const active = getActiveAnnouncements(announcements, authed);
// Spotlight announcements are handled by the blocking spotlight dialog,
// so skip them here to avoid double-notifying the user.
const importantUntoasted = active.filter( const importantUntoasted = active.filter(
(a) => a.isImportant && !isAnnouncementToasted(a.id) (a) => a.isImportant && !a.spotlight && !isAnnouncementToasted(a.id)
); );
for (let i = 0; i < importantUntoasted.length; i++) { for (let i = 0; i < importantUntoasted.length; i++) {

View file

@ -51,7 +51,9 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk =
doc doc
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{isDocsChunk ? "Documentation reference" : "Uploaded document"}</TooltipContent> <TooltipContent>
{isDocsChunk ? "Documentation reference" : "Uploaded document"}
</TooltipContent>
</Tooltip> </Tooltip>
); );
} }

View file

@ -67,6 +67,7 @@ import {
} from "@/components/assistant-ui/inline-mention-editor"; } from "@/components/assistant-ui/inline-mention-editor";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { UserMessage } from "@/components/assistant-ui/user-message"; import { UserMessage } from "@/components/assistant-ui/user-message";
import { ChatExamplePrompts } from "@/components/new-chat/chat-example-prompts";
import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup"; import { ComposerSuggestionPopoverContent } from "@/components/new-chat/composer-suggestion-popup";
import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker"; import { PromptPicker, type PromptPickerRef } from "@/components/new-chat/prompt-picker";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
@ -657,6 +658,15 @@ const Composer: FC = () => {
[actionQuery, aui] [actionQuery, aui]
); );
const handleExampleSelect = useCallback(
(prompt: string) => {
editorRef.current?.setText(prompt);
aui.composer().setText(prompt);
editorRef.current?.focus();
},
[aui]
);
const handleQuickAskSelect = useCallback( const handleQuickAskSelect = useCallback(
(action: { name: string; prompt: string; mode: "transform" | "explore" }) => { (action: { name: string; prompt: string; mode: "transform" | "explore" }) => {
if (!clipboardInitialText) return; if (!clipboardInitialText) return;
@ -916,6 +926,7 @@ const Composer: FC = () => {
isThreadEmpty={isThreadEmpty} isThreadEmpty={isThreadEmpty}
onVisibleChange={setConnectToolsTrayVisible} onVisibleChange={setConnectToolsTrayVisible}
/> />
{isThreadEmpty && <ChatExamplePrompts onSelect={handleExampleSelect} />}
</div> </div>
</ComposerPrimitive.Root> </ComposerPrimitive.Root>
); );

View file

@ -15,6 +15,7 @@ import {
type TokenUsageData, type TokenUsageData,
TokenUsageProvider, TokenUsageProvider,
} from "@/components/assistant-ui/token-usage-context"; } from "@/components/assistant-ui/token-usage-context";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useAnonymousMode } from "@/contexts/anonymous-mode";
import { TimelineDataUI } from "@/features/chat-messages/timeline"; import { TimelineDataUI } from "@/features/chat-messages/timeline";
import { import {
@ -101,11 +102,16 @@ export function FreeChatPage() {
const anonMode = useAnonymousMode(); const anonMode = useAnonymousMode();
const modelSlug = anonMode.isAnonymous ? anonMode.modelSlug : ""; const modelSlug = anonMode.isAnonymous ? anonMode.modelSlug : "";
const resetKey = anonMode.isAnonymous ? anonMode.resetKey : 0; const resetKey = anonMode.isAnonymous ? anonMode.resetKey : 0;
const webSearchEnabled = anonMode.isAnonymous ? anonMode.webSearchEnabled : true;
const [messages, setMessages] = useState<ThreadMessageLike[]>([]); const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [tokenUsageStore] = useState(() => createTokenUsageStore()); const [tokenUsageStore] = useState(() => createTokenUsageStore());
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
// Mirror the latest messages into a ref so onNew stays a stable callback
// (it reads history on demand instead of depending on the array).
const messagesRef = useRef<ThreadMessageLike[]>([]);
messagesRef.current = messages;
// Turnstile CAPTCHA state // Turnstile CAPTCHA state
const [captchaRequired, setCaptchaRequired] = useState(false); const [captchaRequired, setCaptchaRequired] = useState(false);
@ -152,6 +158,7 @@ export function FreeChatPage() {
model_slug: modelSlug, model_slug: modelSlug,
messages: messageHistory, messages: messageHistory,
}; };
if (!webSearchEnabled) reqBody.disabled_tools = ["web_search"];
if (turnstileToken) reqBody.turnstile_token = turnstileToken; if (turnstileToken) reqBody.turnstile_token = turnstileToken;
const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, { const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, {
@ -301,7 +308,7 @@ export function FreeChatPage() {
throw err; throw err;
} }
}, },
[modelSlug, tokenUsageStore] [modelSlug, tokenUsageStore, webSearchEnabled]
); );
const onNew = useCallback( const onNew = useCallback(
@ -345,7 +352,7 @@ export function FreeChatPage() {
}, },
]); ]);
const messageHistory = messages const messageHistory = messagesRef.current
.filter((m) => m.role === "user" || m.role === "assistant") .filter((m) => m.role === "user" || m.role === "assistant")
.map((m) => { .map((m) => {
let text = ""; let text = "";
@ -395,7 +402,7 @@ export function FreeChatPage() {
abortControllerRef.current = null; abortControllerRef.current = null;
} }
}, },
[messages, doStream] [modelSlug, anonMode, doStream]
); );
/** Called when Turnstile resolves successfully. Stores the token and auto-retries. */ /** Called when Turnstile resolves successfully. Stores the token and auto-retries. */
@ -481,19 +488,21 @@ export function FreeChatPage() {
</div> </div>
{captchaRequired && TURNSTILE_SITE_KEY && ( {captchaRequired && TURNSTILE_SITE_KEY && (
<div className="flex flex-col items-center gap-3 border-b border-border/40 bg-muted/30 py-4"> <div className="flex justify-center border-b bg-muted/30 px-4 py-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <Alert className="w-auto max-w-md">
<ShieldCheck className="h-4 w-4" /> <ShieldCheck />
<span>Quick verification to continue chatting</span> <AlertTitle>Quick verification to continue chatting</AlertTitle>
</div> <AlertDescription>
<Turnstile <Turnstile
ref={turnstileRef} ref={turnstileRef}
siteKey={TURNSTILE_SITE_KEY} siteKey={TURNSTILE_SITE_KEY}
onSuccess={handleTurnstileSuccess} onSuccess={handleTurnstileSuccess}
onError={() => turnstileRef.current?.reset()} onError={() => turnstileRef.current?.reset()}
onExpire={() => turnstileRef.current?.reset()} onExpire={() => turnstileRef.current?.reset()}
options={{ theme: "auto", size: "normal" }} options={{ theme: "auto", size: "normal" }}
/> />
</AlertDescription>
</Alert>
</div> </div>
)} )}

View file

@ -6,6 +6,7 @@ import { type FC, useCallback, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useAnonymousMode } from "@/contexts/anonymous-mode";
@ -71,10 +72,11 @@ export const FreeComposer: FC = () => {
const { gate } = useLoginGate(); const { gate } = useLoginGate();
const anonMode = useAnonymousMode(); const anonMode = useAnonymousMode();
const [text, setText] = useState(""); const [text, setText] = useState("");
const [webSearchEnabled, setWebSearchEnabled] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const hasUploadedDoc = anonMode.isAnonymous && anonMode.uploadedDoc !== null; const hasUploadedDoc = anonMode.isAnonymous && anonMode.uploadedDoc !== null;
const webSearchEnabled = anonMode.isAnonymous ? anonMode.webSearchEnabled : true;
const setWebSearchEnabled = anonMode.isAnonymous ? anonMode.setWebSearchEnabled : () => {};
const handleTextChange = useCallback( const handleTextChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => { (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -189,14 +191,11 @@ export const FreeComposer: FC = () => {
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm"
onClick={handleUploadClick} onClick={handleUploadClick}
className={cn( className={cn(hasUploadedDoc && "text-primary")}
"h-auto gap-1.5 rounded-md px-2 py-1 text-xs transition-colors",
"text-muted-foreground hover:text-accent-foreground hover:bg-accent",
hasUploadedDoc && "text-primary"
)}
> >
<Paperclip className="size-3.5" /> <Paperclip data-icon="inline-start" />
{hasUploadedDoc ? "1/1" : "Upload"} {hasUploadedDoc ? "1/1" : "Upload"}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@ -207,13 +206,13 @@ export const FreeComposer: FC = () => {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<div className="h-4 w-px bg-border/60" /> <Separator orientation="vertical" className="h-4" />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<label <label
htmlFor="free-web-search-toggle" htmlFor="free-web-search-toggle"
className="flex items-center gap-1.5 cursor-pointer select-none rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-accent-foreground hover:bg-accent transition-colors" className="flex cursor-pointer select-none items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
> >
<Globe className="size-3.5" /> <Globe className="size-3.5" />
<span className="hidden sm:inline">Web</span> <span className="hidden sm:inline">Web</span>
@ -221,7 +220,6 @@ export const FreeComposer: FC = () => {
id="free-web-search-toggle" id="free-web-search-toggle"
checked={webSearchEnabled} checked={webSearchEnabled}
onCheckedChange={setWebSearchEnabled} onCheckedChange={setWebSearchEnabled}
className="scale-75"
/> />
</label> </label>
</TooltipTrigger> </TooltipTrigger>

View file

@ -1,10 +1,18 @@
"use client"; "use client";
import { Bot, Check, ChevronDown, Search } from "lucide-react"; import { Bot, Check, ChevronDown } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useAnonymousMode } from "@/contexts/anonymous-mode";
import type { AnonModel } from "@/contracts/types/anonymous-chat.types"; import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
@ -19,21 +27,18 @@ export function FreeModelSelector({ className }: { className?: string }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [models, setModels] = useState<AnonModel[]>([]); const [models, setModels] = useState<AnonModel[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [focusedIndex, setFocusedIndex] = useState(-1);
const searchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
anonymousChatApiService.getModels().then(setModels).catch(console.error); const controller = new AbortController();
}, []); anonymousChatApiService
.getModels()
const handleOpenChange = useCallback((next: boolean) => { .then((data) => {
if (next) { if (!controller.signal.aborted) setModels(data);
setSearchQuery(""); })
setFocusedIndex(-1); .catch((err) => {
requestAnimationFrame(() => searchInputRef.current?.focus()); if (!controller.signal.aborted) console.error(err);
} });
setOpen(next); return () => controller.abort();
}, []); }, []);
const currentModel = useMemo( const currentModel = useMemo(
@ -41,22 +46,12 @@ export function FreeModelSelector({ className }: { className?: string }) {
[models, currentSlug] [models, currentSlug]
); );
// Free models first, premium last; immutable sort to avoid mutating state.
const sortedModels = useMemo( const sortedModels = useMemo(
() => [...models].sort((a, b) => Number(a.is_premium) - Number(b.is_premium)), () => models.toSorted((a, b) => Number(a.is_premium) - Number(b.is_premium)),
[models] [models]
); );
const filteredModels = useMemo(() => {
if (!searchQuery.trim()) return sortedModels;
const q = searchQuery.toLowerCase();
return sortedModels.filter(
(m) =>
m.name.toLowerCase().includes(q) ||
m.model_name.toLowerCase().includes(q) ||
m.provider.toLowerCase().includes(q)
);
}, [sortedModels, searchQuery]);
const handleSelect = useCallback( const handleSelect = useCallback(
(model: AnonModel) => { (model: AnonModel) => {
setOpen(false); setOpen(false);
@ -70,42 +65,15 @@ export function FreeModelSelector({ className }: { className?: string }) {
[currentSlug, anonMode, router] [currentSlug, anonMode, router]
); );
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
const count = filteredModels.length;
if (count === 0) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setFocusedIndex((p) => (p < count - 1 ? p + 1 : 0));
break;
case "ArrowUp":
e.preventDefault();
setFocusedIndex((p) => (p > 0 ? p - 1 : count - 1));
break;
case "Enter":
e.preventDefault();
if (focusedIndex >= 0 && focusedIndex < count) {
handleSelect(filteredModels[focusedIndex]);
}
break;
}
},
[filteredModels, focusedIndex, handleSelect]
);
return ( return (
<Popover open={open} onOpenChange={handleOpenChange}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn("gap-2 bg-muted hover:bg-muted/80", className)}
"h-8 gap-2 px-3 text-sm bg-muted hover:bg-muted/80 border-0 select-none",
className
)}
> >
{currentModel ? ( {currentModel ? (
<> <>
@ -118,90 +86,47 @@ export function FreeModelSelector({ className }: { className?: string }) {
<span className="text-muted-foreground">Select Model</span> <span className="text-muted-foreground">Select Model</span>
</> </>
)} )}
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-1 shrink-0" /> <ChevronDown className="ml-1 size-3.5 shrink-0 text-muted-foreground" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent className="w-[320px] p-0" align="start" sideOffset={8}>
className="w-[320px] p-0 rounded-lg shadow-lg overflow-hidden select-none" <Command
align="start" filter={(value, search) => (value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0)}
sideOffset={8} >
onCloseAutoFocus={(e) => e.preventDefault()} <CommandInput placeholder="Search models" />
> <CommandList>
<div className="relative"> <CommandEmpty>No models found.</CommandEmpty>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground pointer-events-none" /> <CommandGroup>
<input {sortedModels.map((model) => {
ref={searchInputRef} const isSelected = model.seo_slug === currentSlug;
placeholder="Search models" return (
value={searchQuery} <CommandItem
onChange={(e) => setSearchQuery(e.target.value)} key={model.id}
onKeyDown={handleKeyDown} value={`${model.name} ${model.model_name} ${model.provider}`}
className="w-full pl-8 pr-3 py-2.5 text-sm bg-transparent focus:outline-none placeholder:text-muted-foreground" onSelect={() => handleSelect(model)}
/> className="gap-2.5"
</div> >
<div className="overflow-y-auto max-h-[320px] py-1 space-y-0.5"> <div className="shrink-0">
{filteredModels.length === 0 ? ( {getProviderIcon(model.provider, { className: "size-5" })}
<div className="flex flex-col items-center justify-center gap-2 py-8 px-4">
<Search className="size-6 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No models found</p>
</div>
) : (
filteredModels.map((model, index) => {
const isSelected = model.seo_slug === currentSlug;
const isFocused = focusedIndex === index;
return (
<div
key={model.id}
role="option"
tabIndex={0}
aria-selected={isSelected}
onClick={() => handleSelect(model)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect(model);
}
}}
onMouseEnter={() => setFocusedIndex(index)}
className={cn(
"group flex items-center gap-2.5 px-3 py-2 rounded-xl cursor-pointer",
"transition-colors duration-150 mx-2",
"hover:bg-accent hover:text-accent-foreground",
isFocused && "bg-accent text-accent-foreground",
isSelected && "bg-accent text-accent-foreground"
)}
>
<div className="shrink-0">
{getProviderIcon(model.provider, { className: "size-5" })}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="font-medium text-sm truncate">{model.name}</span>
{model.is_premium ? (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300 border-0"
>
Premium
</Badge>
) : (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300 border-0"
>
Free
</Badge>
)}
</div> </div>
<span className="text-xs text-muted-foreground truncate block"> <div className="flex min-w-0 flex-1 flex-col">
{model.model_name} <div className="flex items-center gap-1.5">
</span> <span className="truncate text-sm font-medium">{model.name}</span>
</div> <Badge variant={model.is_premium ? "default" : "secondary"}>
{isSelected && <Check className="size-4 text-primary shrink-0" />} {model.is_premium ? "Premium" : "Free"}
</div> </Badge>
); </div>
}) <span className="block truncate text-xs text-muted-foreground">
)} {model.model_name}
</div> </span>
</div>
{isSelected && <Check className="size-4 shrink-0 text-primary" />}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
); );

View file

@ -4,6 +4,14 @@ import { Lock } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import type { FC } from "react"; import type { FC } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
interface GatedTabProps { interface GatedTabProps {
title: string; title: string;
@ -11,16 +19,20 @@ interface GatedTabProps {
} }
const GatedTab: FC<GatedTabProps> = ({ title, description }) => ( const GatedTab: FC<GatedTabProps> = ({ title, description }) => (
<div className="flex flex-col items-center justify-center gap-3 p-8 text-center"> <Empty>
<div className="rounded-full bg-muted p-3"> <EmptyHeader>
<Lock className="size-5 text-muted-foreground" /> <EmptyMedia variant="icon">
</div> <Lock />
<h3 className="text-sm font-medium">{title}</h3> </EmptyMedia>
<p className="text-xs text-muted-foreground max-w-[200px]">{description}</p> <EmptyTitle>{title}</EmptyTitle>
<Button size="sm" asChild> <EmptyDescription>{description}</EmptyDescription>
<Link href="/register">Create Free Account</Link> </EmptyHeader>
</Button> <EmptyContent>
</div> <Button size="sm" asChild>
<Link href="/register">Create Free Account</Link>
</Button>
</EmptyContent>
</Empty>
); );
export const ReportsGatedPlaceholder: FC = () => ( export const ReportsGatedPlaceholder: FC = () => (

View file

@ -2,6 +2,7 @@
import { OctagonAlert, Orbit } from "lucide-react"; import { OctagonAlert, Orbit } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -19,38 +20,30 @@ export function QuotaBar({ used, limit, warningThreshold, className }: QuotaBarP
const isExceeded = used >= limit; const isExceeded = used >= limit;
return ( return (
<div className={cn("space-y-1.5", className)}> <div className={cn("flex flex-col gap-1.5", className)}>
<div className="flex justify-between items-center text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{used.toLocaleString()} / {limit.toLocaleString()} tokens {used.toLocaleString()} / {limit.toLocaleString()} tokens
</span> </span>
{isExceeded ? ( {isExceeded ? (
<span className="font-medium text-red-500">Limit reached</span> <span className="font-medium text-destructive">Limit reached</span>
) : isWarning ? ( ) : isWarning ? (
<span className="font-medium text-amber-500 flex items-center gap-1"> <span className="flex items-center gap-1 font-medium text-highlight">
<OctagonAlert className="h-3 w-3" /> <OctagonAlert className="size-3" />
{remaining.toLocaleString()} remaining {remaining.toLocaleString()} remaining
</span> </span>
) : ( ) : (
<span className="font-medium">{percentage.toFixed(0)}%</span> <span className="font-medium">{percentage.toFixed(0)}%</span>
)} )}
</div> </div>
<Progress <Progress value={percentage} className="h-1.5" />
value={percentage}
className={cn(
"h-1.5",
isExceeded && "[&>div]:bg-red-500",
isWarning && !isExceeded && "[&>div]:bg-amber-500"
)}
/>
{isExceeded && ( {isExceeded && (
<Link <Button asChild size="sm" className="mt-0.5 w-full">
href="/register" <Link href="/register">
className="flex items-center justify-center gap-1.5 rounded-md bg-linear-to-r from-purple-600 to-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-opacity hover:opacity-90" <Orbit data-icon="inline-start" />
> Create free account for 5M more tokens
<Orbit className="h-3 w-3" /> </Link>
Create free account for 5M more tokens </Button>
</Link>
)} )}
</div> </div>
); );

View file

@ -3,6 +3,7 @@
import { OctagonAlert, Orbit, X } from "lucide-react"; import { OctagonAlert, Orbit, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -27,61 +28,46 @@ export function QuotaWarningBanner({
if (isExceeded) { if (isExceeded) {
return ( return (
<div <Alert variant="destructive" className={className}>
className={cn( <OctagonAlert />
"rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/50 p-4", <AlertTitle>Free token limit reached</AlertTitle>
className <AlertDescription>
)} <p>
> You&apos;ve used all {limit.toLocaleString()} free tokens. Create a free account to get
<div className="flex items-start gap-3"> $5 of premium credit and access to all models.
<OctagonAlert className="h-5 w-5 text-red-500 shrink-0 mt-0.5" /> </p>
<div className="flex-1 space-y-2"> <Button asChild size="sm" className="mt-1">
<p className="text-sm font-medium text-red-800 dark:text-red-200"> <Link href="/register">
Free token limit reached <Orbit data-icon="inline-start" />
</p>
<p className="text-xs text-red-600 dark:text-red-300">
You&apos;ve used all {limit.toLocaleString()} free tokens. Create a free account to
get $5 of premium credit and access to all models.
</p>
<Link
href="/register"
className="inline-flex items-center gap-1.5 rounded-md bg-linear-to-r from-purple-600 to-blue-600 px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90"
>
<Orbit className="h-4 w-4" />
Create Free Account Create Free Account
</Link> </Link>
</div> </Button>
</div> </AlertDescription>
</div> </Alert>
); );
} }
return ( return (
<div <Alert variant="warning" className={cn("pr-10", className)}>
className={cn( <OctagonAlert />
"rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/50 p-3", <AlertTitle>Running low on free tokens</AlertTitle>
className <AlertDescription>
)} You&apos;ve used {used.toLocaleString()} of {limit.toLocaleString()} free tokens.{" "}
> <Link href="/register" className="font-medium underline hover:no-underline">
<div className="flex items-center gap-3"> Create an account
<OctagonAlert className="h-4 w-4 text-amber-500 shrink-0" /> </Link>{" "}
<p className="flex-1 text-xs text-amber-700 dark:text-amber-300"> for $5 of premium credit.
You&apos;ve used {used.toLocaleString()} of {limit.toLocaleString()} free tokens.{" "} </AlertDescription>
<Link href="/register" className="font-medium underline hover:no-underline"> <Button
Create an account type="button"
</Link>{" "} variant="ghost"
for $5 of premium credit. size="icon"
</p> onClick={() => setDismissed(true)}
<Button aria-label="Dismiss"
type="button" className="absolute top-2 right-2 size-6"
variant="ghost" >
size="icon" <X />
onClick={() => setDismissed(true)} </Button>
className="size-6 text-amber-400 hover:bg-transparent hover:text-amber-600 dark:hover:text-amber-200" </Alert>
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
); );
} }

View file

@ -1,5 +1,6 @@
import { import {
IconBinaryTree, IconBinaryTree,
IconBolt,
IconMessage, IconMessage,
IconMicrophone, IconMicrophone,
IconSearch, IconSearch,
@ -709,6 +710,236 @@ const AiSortIllustration = () => (
</div> </div>
); );
const AutomationIllustration = () => (
<div className="relative flex w-full h-full min-h-[6rem] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-br from-indigo-50 via-violet-50 to-fuchsia-50 dark:from-indigo-950/20 dark:via-violet-950/20 dark:to-fuchsia-950/20 p-4">
<svg viewBox="0 0 800 200" className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<title>
AI automation flow illustration showing a trigger starting an AI agent that acts across
connectors
</title>
{/* Animated flow connectors */}
<g
className="stroke-violet-500 dark:stroke-violet-400"
strokeWidth="2.5"
fill="none"
opacity="0.7"
>
<path d="M 215 100 L 320 100" strokeDasharray="6,6">
<animate
attributeName="stroke-dashoffset"
from="12"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</path>
<path d="M 480 100 L 585 100" strokeDasharray="6,6">
<animate
attributeName="stroke-dashoffset"
from="12"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</path>
</g>
<g className="fill-violet-500 dark:fill-violet-400" opacity="0.7">
<polygon points="320,100 312,95 312,105" />
<polygon points="585,100 577,95 577,105" />
</g>
{/* Trigger node */}
<g>
<rect
x="40"
y="60"
width="175"
height="80"
rx="14"
className="fill-white dark:fill-neutral-800 stroke-indigo-300 dark:stroke-indigo-700"
strokeWidth="2"
/>
<text
x="127"
y="50"
fontSize="13"
fontWeight="600"
className="fill-indigo-600 dark:fill-indigo-300"
textAnchor="middle"
>
Trigger
</text>
{/* Schedule chip */}
<g transform="translate(58, 80)">
<rect
width="64"
height="22"
rx="11"
className="fill-indigo-100 dark:fill-indigo-900/50"
/>
<circle
cx="14"
cy="11"
r="6"
className="fill-none stroke-indigo-500 dark:stroke-indigo-400"
strokeWidth="2"
/>
<line
x1="14"
y1="11"
x2="14"
y2="7"
className="stroke-indigo-500 dark:stroke-indigo-400"
strokeWidth="2"
strokeLinecap="round"
/>
<line
x1="14"
y1="11"
x2="17"
y2="13"
className="stroke-indigo-500 dark:stroke-indigo-400"
strokeWidth="2"
strokeLinecap="round"
/>
<text
x="38"
y="15"
fontSize="9"
fontWeight="500"
className="fill-indigo-700 dark:fill-indigo-300"
textAnchor="middle"
>
Cron
</text>
</g>
{/* Event chip */}
<g transform="translate(58, 108)">
<rect
width="64"
height="22"
rx="11"
className="fill-fuchsia-100 dark:fill-fuchsia-900/40"
/>
<path
d="M 13 5 L 9 13 L 14 13 L 11 19 L 18 10 L 13 10 Z"
className="fill-fuchsia-500 dark:fill-fuchsia-400"
/>
<text
x="40"
y="15"
fontSize="9"
fontWeight="500"
className="fill-fuchsia-700 dark:fill-fuchsia-300"
textAnchor="middle"
>
Event
</text>
</g>
</g>
{/* AI Agent core */}
<g>
<rect
x="320"
y="50"
width="160"
height="100"
rx="16"
className="fill-white dark:fill-neutral-800 stroke-violet-400 dark:stroke-violet-500"
strokeWidth="2.5"
/>
<text
x="400"
y="40"
fontSize="13"
fontWeight="600"
className="fill-violet-600 dark:fill-violet-300"
textAnchor="middle"
>
AI Agent
</text>
{/* Sparkle */}
<g transform="translate(400, 92)">
<path
d="M 0,-22 L 5,-7 L 20,-5 L 7,5 L 10,20 L 0,12 L -10,20 L -7,5 L -20,-5 L -5,-7 Z"
className="fill-violet-500 dark:fill-violet-400"
opacity="0.9"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0"
to="360"
dur="12s"
repeatCount="indefinite"
/>
</path>
<circle cx="0" cy="0" r="4" className="fill-white dark:fill-violet-200">
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
</circle>
</g>
</g>
{/* Actions across connectors */}
<g>
<rect
x="585"
y="60"
width="175"
height="80"
rx="14"
className="fill-white dark:fill-neutral-800 stroke-fuchsia-300 dark:stroke-fuchsia-700"
strokeWidth="2"
/>
<text
x="672"
y="50"
fontSize="13"
fontWeight="600"
className="fill-fuchsia-600 dark:fill-fuchsia-300"
textAnchor="middle"
>
Act on Connectors
</text>
<g>
<circle cx="618" cy="100" r="13" className="fill-indigo-100 dark:fill-indigo-900/50" />
<circle cx="650" cy="100" r="13" className="fill-violet-100 dark:fill-violet-900/50" />
<circle cx="682" cy="100" r="13" className="fill-fuchsia-100 dark:fill-fuchsia-900/50" />
<circle cx="714" cy="100" r="13" className="fill-pink-100 dark:fill-pink-900/40" />
<text
x="730"
y="104"
fontSize="11"
fontWeight="600"
className="fill-fuchsia-600 dark:fill-fuchsia-300"
textAnchor="middle"
>
25+
</text>
</g>
</g>
{/* Sparkle accents */}
<g className="opacity-60">
<circle cx="270" cy="70" r="2" className="fill-violet-400">
<animate attributeName="opacity" values="0;1;0" dur="2s" repeatCount="indefinite" />
</circle>
<circle cx="530" cy="130" r="2" className="fill-fuchsia-400">
<animate
attributeName="opacity"
values="0;1;0"
dur="2.5s"
begin="0.6s"
repeatCount="indefinite"
/>
</circle>
</g>
</svg>
</div>
);
const items = [ const items = [
{ {
title: "Find, Ask, Act", title: "Find, Ask, Act",
@ -749,4 +980,12 @@ const items = [
className: "md:col-span-1", className: "md:col-span-1",
icon: <IconMessage className="h-4 w-4 text-neutral-500" />, icon: <IconMessage className="h-4 w-4 text-neutral-500" />,
}, },
{
title: "Automate Your Workflows",
description:
"Describe an AI agent in plain English and SurfSense builds it. Run it on a schedule or trigger it when a document lands, acting across all your connectors hands-free.",
header: <AutomationIllustration />,
className: "md:col-span-3",
icon: <IconBolt className="h-4 w-4 text-neutral-500" />,
},
]; ];

View file

@ -1,6 +1,14 @@
"use client"; "use client";
import { ChevronDown, Download, Monitor } from "lucide-react"; import {
import { AnimatePresence, motion } from "motion/react"; ChevronDown,
Clock,
CornerDownLeft,
Download,
Lightbulb,
Monitor,
Sparkles,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import Link from "next/link"; import Link from "next/link";
import React, { memo, useCallback, useEffect, useRef, useState } from "react"; import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer"; import Balancer from "react-wrap-balancer";
@ -11,7 +19,18 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay"; import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { import {
GITHUB_RELEASES_URL, GITHUB_RELEASES_URL,
@ -50,96 +69,215 @@ const GoogleLogo = ({ className }: { className?: string }) => (
</svg> </svg>
); );
const TAB_ITEMS = [ type HeroUseCase = {
id: string;
title: string;
description: string;
src: string | null;
comingSoon?: boolean;
examples?: string[];
};
type HeroCategory = {
id: string;
label: string;
desktopOnly?: boolean;
useCases: HeroUseCase[];
};
const HERO_TUTORIAL = "/homepage/hero_tutorial";
const HERO_REALTIME = "/homepage/hero_realtime";
const CATEGORIES: HeroCategory[] = [
{ {
title: "General Assist", id: "desktop",
description: "Launch SurfSense instantly from any application.", label: "Desktop App",
src: "/homepage/hero_tutorial/general_assist.mp4", desktopOnly: true,
featured: true, useCases: [
{
id: "general",
title: "General Assist",
description: "Launch SurfSense instantly from any application with a global shortcut.",
src: `${HERO_TUTORIAL}/general_assist.mp4`,
},
{
id: "quick",
title: "Quick Assist",
description: "Select text anywhere, then ask AI to explain, rewrite, or act on it.",
src: `${HERO_TUTORIAL}/quick_assist.mp4`,
},
{
id: "screenshot",
title: "Screenshot Assist",
description: "Capture any region of your screen and ask AI about whats in it.",
src: `${HERO_TUTORIAL}/screenshot_assist.mp4`,
},
{
id: "watch-folder",
title: "Watch Local Folder",
description: "Auto-sync a local folder to your knowledge base. Great for Obsidian vaults.",
src: `${HERO_TUTORIAL}/folder_watch.mp4`,
},
],
}, },
{ {
title: "Quick Assist", id: "deliverables",
description: "Select text anywhere, then ask AI to explain, rewrite, or act on it.", label: "Deliverable Studio",
src: "/homepage/hero_tutorial/quick_assist.mp4", useCases: [
featured: true, {
id: "report",
title: "AI Report Generator",
description:
"Generate cited research reports from your documents, then export to PDF or Markdown.",
src: `${HERO_TUTORIAL}/ReportGenGif_compressed.mp4`,
},
{
id: "podcast",
title: "AI Podcast Generator",
description: "Turn any document or folder into a two-host AI podcast in under 20 seconds.",
src: `${HERO_TUTORIAL}/PodcastGenGif.mp4`,
},
{
id: "presentation",
title: "AI Presentation & Video Maker",
description: "Create editable slide decks and narrated video overviews from your sources.",
src: `${HERO_TUTORIAL}/video_gen_surf.mp4`,
},
{
id: "image",
title: "AI Image Generator",
description: "Generate high-quality images straight from your chats and documents.",
src: `${HERO_TUTORIAL}/ImageGenGif.mp4`,
},
{
id: "resume",
title: "AI Resume Builder",
description: "Tailor your existing resume to any job description and beat the ATS.",
src: null,
comingSoon: true,
examples: [
"Tailor my resume to this job description so it gets past ATS and lands an interview.",
"Optimize my resume for ATS by matching the keywords in this job posting.",
"Rewrite my resume bullet points to highlight the skills this role is asking for.",
"Compare my resume against this job description and list the gaps to fix.",
"Write a matching cover letter from my resume and this job description.",
],
},
],
}, },
{ {
title: "Screenshot Assist", id: "automations",
description: label: "Automations",
"Use a global shortcut to select a region on your screen and attach it to your chat message.", useCases: [
src: "/homepage/hero_tutorial/screenshot_assist.mp4", {
featured: true, id: "schedule",
title: "Scheduled AI Workflows",
description: "Run an agent on a schedule: daily briefs, weekly digests, recurring reports.",
src: null,
comingSoon: true,
examples: [
"Email me a daily brief of new documents in my knowledge base every morning.",
"Generate a weekly status report from my Slack and Gmail every Friday.",
"Run a monthly competitor analysis report and save it to my workspace.",
"Summarize my GitHub and Linear activity into a daily standup update.",
"Create a recurring weekly research report on the topics I track.",
],
},
{
id: "event",
title: "Event-Triggered Automations",
description:
"Fire an agent the moment a document lands in a folder, then post the result to your tools.",
src: null,
comingSoon: true,
examples: [
"When a PDF lands in my Research folder, generate a cited AI summary.",
"When new meeting notes are added, turn them into meeting minutes with action items.",
"When an invoice is uploaded, extract the vendor, total, and due date into a table.",
"When a contract enters my Legal folder, flag key terms and renewal dates.",
"When a resume is added to Candidates, screen it against the job description.",
],
},
{
id: "chat-built",
title: "Chat-Built Automations",
description: "Describe an automation in plain English and SurfSense builds it for you.",
src: null,
comingSoon: true,
examples: [
"Build an AI agent that emails me a summary of new Notion pages each morning.",
"Create a no-code automation that posts a weekly research digest to Slack.",
"Set up an AI note taker that turns new meeting notes into minutes.",
"Make a workflow that extracts action items from meeting notes and assigns owners.",
"Automate a daily email brief from my Gmail and Google Drive.",
],
},
],
}, },
{ {
title: "Watch Local Folder", id: "search-chat",
description: label: "Search & Chat",
"Watch a local folder and automatically sync file changes to your knowledge base. Works great with Obsidian vaults.", useCases: [
src: "/homepage/hero_tutorial/folder_watch.mp4", {
featured: true, id: "chat-docs",
}, title: "Chat With Your PDFs & Docs",
// { description: "Ask questions across all your files and get answers with inline citations.",
// title: "Connect & Sync", src: `${HERO_TUTORIAL}/BQnaGif_compressed.mp4`,
// description: },
// "Connect data sources like Notion, Drive and Gmail. Automatically sync to keep them updated.", {
// src: "/homepage/hero_tutorial/ConnectorFlowGif.mp4", id: "search",
// featured: true, title: "AI Search With Citations",
// }, description: "Hybrid semantic and keyword search across your entire knowledge base.",
// { src: `${HERO_TUTORIAL}/BSNCGif.mp4`,
// title: "Upload Documents", },
// description: "Upload documents directly, from images to massive PDFs.", {
// src: "/homepage/hero_tutorial/DocUploadGif.mp4", id: "collab",
// featured: true, title: "Collaborative AI Chat",
// }, description: "Work on AI conversations with your team in real time.",
{ src: `${HERO_REALTIME}/RealTimeChatGif.mp4`,
title: "Video & Presentations", },
description: {
"Create short videos and editable presentations with AI-generated visuals and narration from your sources.", id: "comments",
src: "/homepage/hero_tutorial/video_gen_surf.mp4", title: "Comments & Mentions",
featured: false, description: "Comment and tag teammates on any AI message.",
src: `${HERO_REALTIME}/RealTimeCommentsFlow.mp4`,
},
],
}, },
{ {
title: "Search & Citation", id: "connectors",
description: "Ask questions and get cited responses from your knowledge base.", label: "Connectors & Integrations",
src: "/homepage/hero_tutorial/BSNCGif.mp4", useCases: [
featured: false, {
id: "connect",
title: "Connect & Sync Your Tools",
description:
"Sync Notion, Slack, Google Drive, Gmail, GitHub, Linear and 25+ sources into one searchable corpus.",
src: `${HERO_TUTORIAL}/ConnectorFlowGif.mp4`,
},
{
id: "upload",
title: "Chat With Uploaded Files",
description: "Drop in PDFs, Office docs, images and audio. Instantly searchable.",
src: `${HERO_TUTORIAL}/DocUploadGif.mp4`,
},
{
id: "write-back",
title: "Connector Write-Back",
description: "Let the agent post results back to Notion, Slack, Linear and Drive.",
src: null,
comingSoon: true,
examples: [
"Post this research summary to my Notion workspace.",
"Send these meeting action items to our team Slack channel.",
"Create a Jira ticket from this bug report.",
"Open a Linear issue from this feature request.",
"Save this generated report to Google Drive as a doc.",
],
},
],
}, },
{ ];
title: "Document Q&A",
description: "Mention specific documents in chat for targeted answers.",
src: "/homepage/hero_tutorial/BQnaGif_compressed.mp4",
featured: false,
},
{
title: "Reports",
description: "Generate reports from your sources in many formats.",
src: "/homepage/hero_tutorial/ReportGenGif_compressed.mp4",
featured: false,
},
{
title: "Podcasts",
description: "Turn anything into a podcast in under 20 seconds.",
src: "/homepage/hero_tutorial/PodcastGenGif.mp4",
featured: false,
},
{
title: "Image Generation",
description: "Generate high-quality images easily from your conversations.",
src: "/homepage/hero_tutorial/ImageGenGif.mp4",
featured: false,
},
{
title: "Collaborative Chat",
description: "Collaborate on AI-powered conversations in realtime with your team.",
src: "/homepage/hero_realtime/RealTimeChatGif.mp4",
featured: false,
},
{
title: "Comments",
description: "Add comments and tag teammates on any message.",
src: "/homepage/hero_realtime/RealTimeCommentsFlow.mp4",
featured: false,
},
] as const;
export function HeroSection() { export function HeroSection() {
return ( return (
@ -279,117 +417,15 @@ function DownloadButton() {
); );
} }
const BrowserWindow = () => { const TabVideo = memo(function TabVideo({
const [selectedIndex, setSelectedIndex] = useState(0); src,
const selectedItem = TAB_ITEMS[selectedIndex]; title,
const { expanded, open, close } = useExpandedMedia(); reduceMotion,
}: {
return ( src: string;
<> title: string;
<motion.div className="relative my-4 flex w-full flex-col items-start justify-start overflow-hidden rounded-2xl shadow-2xl md:my-12"> reduceMotion: boolean;
<div className="flex w-full items-center justify-start overflow-hidden bg-gray-200 py-4 pl-4 dark:bg-neutral-800"> }) {
<div className="mr-6 flex items-center gap-2">
<div className="size-3 rounded-full bg-red-500" />
<div className="size-3 rounded-full bg-yellow-500" />
<div className="size-3 rounded-full bg-green-500" />
</div>
<div className="no-visible-scrollbar flex min-w-0 shrink flex-row items-center justify-start gap-2 overflow-x-auto mask-l-from-98% py-0.5 pr-2 pl-2 md:pl-4">
{TAB_ITEMS.map((item, index) => (
<React.Fragment key={item.title}>
<Button
type="button"
variant="ghost"
onClick={() => setSelectedIndex(index)}
className={cn(
"h-auto shrink-0 gap-1.5 rounded-md px-2 py-1 text-xs transition duration-150 hover:bg-white sm:text-sm dark:hover:bg-neutral-950",
selectedIndex === index &&
!item.featured &&
"bg-white shadow ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-900",
selectedIndex === index &&
item.featured &&
"bg-amber-50 shadow ring-1 shadow-amber-200/50 ring-amber-400/60 dark:bg-amber-950/40 dark:shadow-amber-900/30 dark:ring-amber-500/50",
item.featured &&
selectedIndex !== index &&
"hover:bg-amber-50 dark:hover:bg-amber-950/30"
)}
>
{item.title}
{item.featured && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 items-center justify-center rounded border border-amber-300 bg-amber-100 p-0.5 text-amber-700 dark:border-amber-700 dark:bg-amber-900/50 dark:text-amber-400">
<Monitor className="size-3" />
</span>
</TooltipTrigger>
<TooltipContent side="bottom">Desktop app only</TooltipContent>
</Tooltip>
)}
</Button>
{index !== TAB_ITEMS.length - 1 && (
<div className="h-4 w-px shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
)}
</React.Fragment>
))}
</div>
</div>
<div className="w-full overflow-hidden bg-gray-100/50 px-4 pt-4 perspective-distant dark:bg-neutral-950">
<AnimatePresence mode="wait">
<motion.div
initial={{
opacity: 0,
scale: 0.99,
filter: "blur(10px)",
}}
animate={{
opacity: 1,
scale: 1,
filter: "blur(0px)",
}}
exit={{
opacity: 0,
scale: 0.98,
filter: "blur(10px)",
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
key={selectedItem.title}
className="relative overflow-hidden rounded-tl-xl rounded-tr-xl bg-white shadow-sm ring-1 shadow-black/10 ring-black/10 will-change-transform dark:bg-neutral-950"
>
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
<div className="min-w-0">
<h3 className="truncate text-base font-semibold text-neutral-900 sm:text-lg dark:text-white">
{selectedItem.title}
</h3>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{selectedItem.description}
</p>
</div>
</div>
<Button
type="button"
variant="ghost"
className="h-auto w-full cursor-pointer rounded-none bg-neutral-50 p-2 hover:bg-neutral-50 sm:p-3 dark:bg-neutral-950 dark:hover:bg-neutral-950"
onClick={open}
>
<TabVideo key={selectedItem.src} src={selectedItem.src} />
</Button>
</motion.div>
</AnimatePresence>
</div>
</motion.div>
<AnimatePresence>
{expanded && (
<ExpandedMediaOverlay src={selectedItem.src} alt={selectedItem.title} onClose={close} />
)}
</AnimatePresence>
</>
);
};
const TabVideo = memo(function TabVideo({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const [hasLoaded, setHasLoaded] = useState(false); const [hasLoaded, setHasLoaded] = useState(false);
@ -398,8 +434,11 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
video.currentTime = 0; video.currentTime = 0;
video.play().catch(() => {}); // Respect reduced-motion: show the first frame and expose controls instead of autoplaying.
}, []); if (!reduceMotion) {
video.play().catch(() => {});
}
}, [reduceMotion]);
const handleCanPlay = useCallback(() => { const handleCanPlay = useCallback(() => {
setHasLoaded(true); setHasLoaded(true);
@ -411,7 +450,10 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
ref={videoRef} ref={videoRef}
key={src} key={src}
src={src} src={src}
preload="auto" preload={reduceMotion ? "metadata" : "auto"}
aria-label={`${title} demo`}
autoPlay={!reduceMotion}
controls={reduceMotion}
loop loop
muted muted
playsInline playsInline
@ -419,8 +461,233 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) {
className="aspect-video w-full rounded-lg sm:rounded-xl" className="aspect-video w-full rounded-lg sm:rounded-xl"
/> />
{!hasLoaded && ( {!hasLoaded && (
<div className="absolute inset-0 aspect-video w-full animate-pulse rounded-lg bg-neutral-100 sm:rounded-xl dark:bg-neutral-800" /> <Skeleton className="absolute inset-0 aspect-video w-full rounded-lg bg-neutral-100 motion-reduce:animate-none sm:rounded-xl dark:bg-neutral-800" />
)} )}
</div> </div>
); );
}); });
const UseCasePlaceholder = ({ title }: { title: string }) => (
<Empty className="size-full justify-center rounded-lg border border-dashed bg-muted/30 sm:rounded-xl">
<EmptyHeader>
<EmptyMedia variant="icon">
<Clock aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Demo coming soon</EmptyTitle>
<EmptyDescription className="text-pretty">{`A walkthrough of ${title} is on the way.`}</EmptyDescription>
</EmptyHeader>
</Empty>
);
const UseCaseExamples = ({ examples }: { examples: string[] }) => (
<div className="flex size-full flex-col gap-3 rounded-lg border border-dashed bg-muted/30 p-4 sm:rounded-xl sm:p-5">
<div className="flex items-center gap-2">
<Lightbulb aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
<p className="text-sm font-medium text-foreground">Try prompts like these today</p>
</div>
<ul className="flex min-w-0 flex-col gap-2">
{examples.map((example) => (
<li key={example}>
<div className="flex items-start gap-2.5 rounded-md border bg-background px-3 py-2">
<CornerDownLeft
aria-hidden="true"
className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70"
/>
<span className="min-w-0 text-sm text-pretty text-muted-foreground">{example}</span>
</div>
</li>
))}
</ul>
</div>
);
const DesktopBadge = () => (
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-0.5 inline-flex items-center text-amber-600 dark:text-amber-400">
<Monitor aria-hidden="true" className="size-3.5" />
<span className="sr-only">Desktop app only</span>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">Desktop app only</TooltipContent>
</Tooltip>
);
const UseCasePane = memo(function UseCasePane({
useCase,
reduceMotion,
}: {
useCase: HeroUseCase;
reduceMotion: boolean;
}) {
const { expanded, open, close } = useExpandedMedia();
const hasVideo = !useCase.comingSoon && Boolean(useCase.src);
const media = hasVideo ? (
<Button
type="button"
variant="ghost"
onClick={open}
aria-label={`Expand ${useCase.title} demo`}
className="h-auto w-full cursor-pointer rounded-none bg-neutral-50 p-2 hover:bg-neutral-50 sm:p-3 dark:bg-neutral-950 dark:hover:bg-neutral-950"
>
<TabVideo src={useCase.src as string} title={useCase.title} reduceMotion={reduceMotion} />
</Button>
) : (
<div className="bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950">
{useCase.examples && useCase.examples.length > 0 ? (
<UseCaseExamples examples={useCase.examples} />
) : (
<div className="aspect-video w-full">
<UseCasePlaceholder title={useCase.title} />
</div>
)}
</div>
);
const card = (
<div className="relative overflow-hidden rounded-tl-xl rounded-tr-xl bg-white shadow-sm ring-1 shadow-black/10 ring-black/10 dark:bg-neutral-950">
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
<div className="min-w-0">
<h3 className="truncate text-base font-semibold text-neutral-900 sm:text-lg dark:text-white">
{useCase.title}
</h3>
<p className="text-sm text-neutral-500 text-pretty dark:text-neutral-400">
{useCase.description}
</p>
</div>
</div>
{media}
</div>
);
return (
<>
{reduceMotion ? (
card
) : (
<motion.div
initial={{ opacity: 0, scale: 0.99, filter: "blur(10px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="will-change-transform"
>
{card}
</motion.div>
)}
<AnimatePresence>
{expanded && hasVideo && (
<ExpandedMediaOverlay
src={useCase.src as string}
alt={`${useCase.title} demo`}
onClose={close}
/>
)}
</AnimatePresence>
</>
);
});
const CategoryPanel = memo(function CategoryPanel({
category,
reduceMotion,
}: {
category: HeroCategory;
reduceMotion: boolean;
}) {
return (
<div className="flex w-full flex-col gap-3">
{category.desktopOnly && (
<div className="flex items-start gap-2 rounded-lg border border-amber-300/60 bg-amber-50 px-3 py-2 text-xs text-amber-800 sm:text-sm dark:border-amber-500/40 dark:bg-amber-950/30 dark:text-amber-200">
<Sparkles aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
<span className="text-pretty">
The desktop app includes everything in SurfSense, plus these native-only superpowers.
</span>
</div>
)}
<Tabs
defaultValue={category.useCases[0]?.id}
orientation="vertical"
className="flex w-full flex-col gap-3 md:flex-row md:gap-4"
>
<ScrollArea className="w-full md:w-56 md:shrink-0">
<TabsList className="flex h-auto w-max gap-1 bg-transparent p-0 md:w-full md:flex-col md:items-stretch">
{category.useCases.map((useCase) => (
<TabsTrigger
key={useCase.id}
value={useCase.id}
className="h-auto shrink-0 touch-manipulation justify-start rounded-md px-3 py-2 text-left text-xs whitespace-normal data-[state=active]:bg-background data-[state=active]:shadow-sm sm:text-sm md:w-full"
>
{useCase.title}
</TabsTrigger>
))}
</TabsList>
<ScrollBar orientation="horizontal" className="md:hidden" />
</ScrollArea>
<div className="min-w-0 flex-1">
{category.useCases.map((useCase) => (
<TabsContent key={useCase.id} value={useCase.id} className="mt-0">
<UseCasePane useCase={useCase} reduceMotion={reduceMotion} />
</TabsContent>
))}
</div>
</Tabs>
</div>
);
});
const BrowserWindow = () => {
const [activeCategory, setActiveCategory] = useState(CATEGORIES[0].id);
const reduceMotion = useReducedMotion() ?? false;
return (
<Tabs
value={activeCategory}
onValueChange={setActiveCategory}
className="relative my-4 flex w-full flex-col items-start justify-start gap-0 overflow-hidden rounded-2xl shadow-2xl md:my-12"
>
<div className="flex w-full items-center justify-start overflow-hidden bg-gray-200 py-4 pl-4 dark:bg-neutral-800">
<div className="mr-6 flex items-center gap-2">
<div className="size-3 rounded-full bg-red-500" />
<div className="size-3 rounded-full bg-yellow-500" />
<div className="size-3 rounded-full bg-green-500" />
</div>
<ScrollArea className="min-w-0 flex-1">
<TabsList className="flex h-auto w-max items-center gap-1 bg-transparent p-0 pr-4">
{CATEGORIES.map((category, index) => (
<React.Fragment key={category.id}>
<TabsTrigger
value={category.id}
className={cn(
"h-auto shrink-0 touch-manipulation gap-1.5 rounded-md px-2.5 py-1 text-xs sm:text-sm",
category.desktopOnly
? "bg-amber-100/70 text-amber-800 hover:bg-amber-100 data-[state=active]:bg-amber-200/80 data-[state=active]:text-amber-900 data-[state=active]:shadow-sm dark:bg-amber-950/40 dark:text-amber-200 dark:hover:bg-amber-900/40 dark:data-[state=active]:bg-amber-900/60 dark:data-[state=active]:text-amber-50"
: "data-[state=active]:bg-background data-[state=active]:shadow"
)}
>
{category.label}
{category.desktopOnly && <DesktopBadge />}
</TabsTrigger>
{index !== CATEGORIES.length - 1 && (
<Separator
orientation="vertical"
className="h-4 bg-neutral-300 dark:bg-neutral-700"
/>
)}
</React.Fragment>
))}
</TabsList>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
<div className="w-full overflow-hidden bg-gray-100/50 px-4 pt-4 dark:bg-neutral-950">
{CATEGORIES.map((category) => (
<TabsContent key={category.id} value={category.id} className="mt-0">
<CategoryPanel category={category} reduceMotion={reduceMotion} />
</TabsContent>
))}
</div>
</Tabs>
);
};

View file

@ -348,6 +348,11 @@ const comparisonRows: {
notebookLm: false, notebookLm: false,
surfSense: true, surfSense: true,
}, },
{
feature: "AI Automations & Agents",
notebookLm: false,
surfSense: "Scheduled & event-triggered",
},
{ {
feature: "AI File Sorting", feature: "AI File Sorting",
notebookLm: false, notebookLm: false,

View file

@ -18,6 +18,7 @@ import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom"; import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog"; import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog";
import { AnnouncementSpotlight } from "@/components/announcements/AnnouncementSpotlight";
import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog"; import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
import { import {
AlertDialog, AlertDialog,
@ -909,6 +910,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
/> />
<AnnouncementsDialog /> <AnnouncementsDialog />
<AnnouncementSpotlight />
{/* Agent action log + revert dialog */} {/* Agent action log + revert dialog */}
<ActionLogDialog /> <ActionLogDialog />

View file

@ -89,10 +89,7 @@ const DesktopLocalTabContent = dynamic(
{ ssr: false } { ssr: false }
); );
const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = [ const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = ["USER_MEMORY", "TEAM_MEMORY"];
"USER_MEMORY",
"TEAM_MEMORY",
];
const MEMORY_DOCUMENTS: DocumentNodeDoc[] = [ const MEMORY_DOCUMENTS: DocumentNodeDoc[] = [
{ {
id: -1001, id: -1001,

View file

@ -0,0 +1,75 @@
"use client";
import { CornerDownLeft, Lightbulb } from "lucide-react";
import { memo, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CHAT_EXAMPLE_CATEGORIES } from "@/lib/chat/example-prompts";
interface ChatExamplePromptsProps {
/** Called with the chosen prompt text; the caller prefills the composer. */
onSelect: (prompt: string) => void;
}
const ExamplePromptButton = memo(function ExamplePromptButton({
prompt,
onSelect,
}: {
prompt: string;
onSelect: (prompt: string) => void;
}) {
const handleClick = useCallback(() => onSelect(prompt), [prompt, onSelect]);
return (
<Button
type="button"
variant="ghost"
onClick={handleClick}
className="h-auto w-full items-start justify-start gap-2.5 whitespace-normal rounded-md border bg-background px-3 py-2 text-left font-normal text-muted-foreground hover:bg-accent hover:text-accent-foreground"
>
<CornerDownLeft
aria-hidden="true"
className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70"
/>
<span className="min-w-0 text-pretty text-sm">{prompt}</span>
</Button>
);
});
export function ChatExamplePrompts({ onSelect }: ChatExamplePromptsProps) {
return (
<div className="mt-3 w-full select-none rounded-xl border border-dashed bg-muted/30 p-3 sm:p-4">
<div className="mb-2 flex items-center gap-2 px-1">
<Lightbulb aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
<p className="text-sm font-medium text-foreground">
Not sure where to start? Try one of these
</p>
</div>
<Tabs defaultValue={CHAT_EXAMPLE_CATEGORIES[0].id} className="w-full">
<div className="overflow-x-auto pb-1">
<TabsList className="h-9 w-max">
{CHAT_EXAMPLE_CATEGORIES.map((category) => (
<TabsTrigger key={category.id} value={category.id} className="text-xs">
{category.label}
</TabsTrigger>
))}
</TabsList>
</div>
{CHAT_EXAMPLE_CATEGORIES.map((category) => (
<TabsContent key={category.id} value={category.id} className="mt-3">
<ScrollArea className="max-h-48">
<ul className="flex flex-col gap-2 pr-2">
{category.prompts.map((prompt) => (
<li key={prompt}>
<ExamplePromptButton prompt={prompt} onSelect={onSelect} />
</li>
))}
</ul>
</ScrollArea>
</TabsContent>
))}
</Tabs>
</div>
);
}

View file

@ -220,13 +220,7 @@ export const DocumentMentionPicker = forwardRef<
DocumentMentionPickerRef, DocumentMentionPickerRef,
DocumentMentionPickerProps DocumentMentionPickerProps
>(function DocumentMentionPicker( >(function DocumentMentionPicker(
{ { searchSpaceId, onSelectionChange, onDone, initialSelectedDocuments = [], externalSearch = "" },
searchSpaceId,
onSelectionChange,
onDone,
initialSelectedDocuments = [],
externalSearch = "",
},
ref ref
) { ) {
const search = externalSearch; const search = externalSearch;

View file

@ -4,6 +4,7 @@ import { AnimatePresence, motion } from "motion/react";
import type React from "react"; import type React from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Pricing } from "@/components/pricing"; import { Pricing } from "@/components/pricing";
import { FAQJsonLd } from "@/components/seo/json-ld";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -19,6 +20,8 @@ const demoPlans = [
"500 pages included to start", "500 pages included to start",
"$5 in premium credits for paid AI models and premium AI features", "$5 in premium credits for paid AI models and premium AI features",
"Includes access to OpenAI text, audio and image models", "Includes access to OpenAI text, audio and image models",
"AI automations and agents: scheduled and event-triggered workflows",
"Desktop app: Quick, General and Screenshot Assist plus local folder sync",
"Realtime Collaborative Group Chats with teammates", "Realtime Collaborative Group Chats with teammates",
"Community support on Discord", "Community support on Discord",
], ],
@ -37,6 +40,7 @@ const demoPlans = [
"Everything in Free", "Everything in Free",
"Buy 1,000-page packs or $1 in premium credits at $1 each", "Buy 1,000-page packs or $1 in premium credits at $1 each",
"Use premium AI models like GPT-5.4, Claude Sonnet 4.6, Gemini 2.5 Pro & 100+ more via OpenRouter", "Use premium AI models like GPT-5.4, Claude Sonnet 4.6, Gemini 2.5 Pro & 100+ more via OpenRouter",
"Connector write-back to Notion, Slack, Linear & Jira",
"Priority support on Discord", "Priority support on Discord",
], ],
description: "", description: "",
@ -52,6 +56,7 @@ const demoPlans = [
billingText: "", billingText: "",
features: [ features: [
"Everything in Pay As You Go", "Everything in Pay As You Go",
"Custom automation and agent workflows",
"On-prem or VPC deployment", "On-prem or VPC deployment",
"Audit logs and compliance", "Audit logs and compliance",
"SSO, OIDC & SAML", "SSO, OIDC & SAML",
@ -158,6 +163,31 @@ const faqData: FAQSection[] = [
}, },
], ],
}, },
{
title: "Automations & Agents",
items: [
{
question: "What can AI automations and agents do?",
answer:
"AI automations let you run agents on your knowledge base without writing code. You can schedule recurring workflows like daily briefs, weekly status reports, and competitor analysis, or trigger an agent the moment a document lands in a folder. Agents can read across your connected tools, generate summaries and reports, and write results back to Notion, Slack, Linear, and Jira.",
},
{
question: "Do automations and agents cost extra?",
answer:
"No. There is no separate subscription or add-on fee for automations. Agents use the same page credits and premium credits as the rest of SurfSense. Indexing documents consumes page credits, and premium AI model usage during a workflow consumes premium credits at provider cost. If a workflow only uses free models, it does not touch your premium credits.",
},
{
question: "How do event-triggered automations work?",
answer:
"Event-triggered automations fire when something happens in your knowledge base, most commonly when a new document enters a folder you are watching. For example, when a PDF lands in your Research folder you can auto-generate a cited summary, or when an invoice is uploaded you can extract the vendor, total, and due date. The agent runs automatically and can post the result to your connected tools.",
},
{
question: "Can I build an automation without code?",
answer:
"Yes. You can describe the workflow automation you want in plain English in chat, and SurfSense builds the automation for you. For example, ask it to email you a summary of new Notion pages each morning, or post a weekly research digest to Slack, and it sets up the scheduled or event-triggered agent without any code.",
},
],
},
{ {
title: "Self-Hosting", title: "Self-Hosting",
items: [ items: [
@ -250,6 +280,7 @@ function PricingFAQ() {
return ( return (
<div className="mx-auto w-full max-w-4xl overflow-hidden px-4 py-20 md:px-8 md:py-32"> <div className="mx-auto w-full max-w-4xl overflow-hidden px-4 py-20 md:px-8 md:py-32">
<FAQJsonLd questions={faqData.flatMap((section) => section.items)} />
<div className="text-center"> <div className="text-center">
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl"> <h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
Frequently Asked Questions Frequently Asked Questions
@ -341,7 +372,7 @@ function PricingBasic() {
<Pricing <Pricing
plans={demoPlans} plans={demoPlans}
title="SurfSense Pricing" title="SurfSense Pricing"
description="Start free with 500 pages & $5 in premium credits. Pay as you go." description="Start free with 500 pages & $5 in premium credits. Run AI automations and agents, and pay as you go."
/> />
<PricingFAQ /> <PricingFAQ />
</> </>

View file

@ -77,6 +77,9 @@ export function SoftwareApplicationJsonLd() {
"Free access to ChatGPT, Claude AI, and any AI model", "Free access to ChatGPT, Claude AI, and any AI model",
"AI-powered semantic search across all connected tools", "AI-powered semantic search across all connected tools",
"Federated search across Slack, Google Drive, Notion, Confluence, GitHub", "Federated search across Slack, Google Drive, Notion, Confluence, GitHub",
"AI automations and agents (scheduled and event-triggered workflows)",
"Connector write-back to Notion, Slack, Linear, Jira",
"Native desktop app with Quick, General, and Screenshot Assist",
"No data limits with open source self-hosting", "No data limits with open source self-hosting",
"Real-time collaborative team chats", "Real-time collaborative team chats",
"Document Q&A with citations", "Document Q&A with citations",

View file

@ -0,0 +1,94 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn("flex max-w-sm flex-col items-center gap-2 text-center", className)}
{...props}
/>
);
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
);
}
export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };

View file

@ -9,6 +9,8 @@ export interface AnonymousModeContextValue {
setModelSlug: (slug: string) => void; setModelSlug: (slug: string) => void;
uploadedDoc: { filename: string; sizeBytes: number } | null; uploadedDoc: { filename: string; sizeBytes: number } | null;
setUploadedDoc: (doc: { filename: string; sizeBytes: number } | null) => void; setUploadedDoc: (doc: { filename: string; sizeBytes: number } | null) => void;
webSearchEnabled: boolean;
setWebSearchEnabled: (enabled: boolean) => void;
resetKey: number; resetKey: number;
resetChat: () => void; resetChat: () => void;
} }
@ -34,6 +36,7 @@ export function AnonymousModeProvider({
const [uploadedDoc, setUploadedDoc] = useState<{ filename: string; sizeBytes: number } | null>( const [uploadedDoc, setUploadedDoc] = useState<{ filename: string; sizeBytes: number } | null>(
null null
); );
const [webSearchEnabled, setWebSearchEnabled] = useState(true);
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const resetChat = () => setResetKey((k) => k + 1); const resetChat = () => setResetKey((k) => k + 1);
@ -56,10 +59,12 @@ export function AnonymousModeProvider({
setModelSlug, setModelSlug,
uploadedDoc, uploadedDoc,
setUploadedDoc, setUploadedDoc,
webSearchEnabled,
setWebSearchEnabled,
resetKey, resetKey,
resetChat, resetChat,
}), }),
[modelSlug, uploadedDoc, resetKey] [modelSlug, uploadedDoc, webSearchEnabled, resetKey]
); );
return <AnonymousModeContext.Provider value={value}>{children}</AnonymousModeContext.Provider>; return <AnonymousModeContext.Provider value={value}>{children}</AnonymousModeContext.Provider>;

View file

@ -35,6 +35,20 @@ export interface Announcement {
audience: AnnouncementAudience; audience: AnnouncementAudience;
/** If true, the user will see a toast notification for this announcement */ /** If true, the user will see a toast notification for this announcement */
isImportant: boolean; isImportant: boolean;
/**
* If true, this announcement is shown in a blocking spotlight dialog that the
* user must explicitly acknowledge ("Got it"). Until acknowledged it keeps
* reappearing; once acknowledged it never shows again. Spotlight announcements
* are skipped by the lightweight toast provider to avoid double notifications.
*/
spotlight?: boolean;
/** Optional head/banner image shown at the top of the announcement */
image?: {
/** Image source (public path or absolute URL) */
src: string;
/** Accessible alt text */
alt: string;
};
/** Optional CTA link */ /** Optional CTA link */
link?: { link?: {
label: string; label: string;

View file

@ -13,6 +13,27 @@ import type { Announcement } from "@/contracts/types/announcement.types";
* This file can be replaced with an API call in the future. * This file can be replaced with an API call in the future.
*/ */
export const announcements: Announcement[] = [ export const announcements: Announcement[] = [
{
id: "2026-05-31-ai-automations",
title: "Introducing AI Automations",
description:
"Turn prompts into hands-off AI agent workflows. Describe an automation in plain English and SurfSense builds it, run it on a schedule, or trigger it the moment a document lands in a folder. Automations work across Notion, Slack, Google Drive, Gmail, GitHub, Linear, Jira and more.",
category: "feature",
date: "2026-05-31T00:00:00Z",
startTime: "2026-05-31T00:00:00Z",
endTime: "2026-07-15T00:00:00Z",
audience: "users",
isImportant: true,
spotlight: true,
image: {
src: "/announcements/automations.png",
alt: "Connector tiles flowing into a central AI core that triggers scheduled and event-driven automations.",
},
link: {
label: "See what's new",
url: "/changelog",
},
},
{ {
id: "announcement-1", id: "announcement-1",
title: "Introducing What's New", title: "Introducing What's New",

View file

@ -0,0 +1,66 @@
/**
* Curated example chat prompts shown on the empty new-chat screen.
*
* These mirror the homepage hero's "use case" concept but with runnable chat
* queries, grouped into a few broad categories. Bracketed slots like `[topic]`
* are intentional: clicking a prompt prefills the composer so the user can fill
* them in before sending.
*
* This is a module-scope constant so it is created once, not per render.
*/
export interface ChatExampleCategory {
/** Stable id used as the Tabs value */
id: string;
/** Short, human-readable tab label */
label: string;
/** Runnable example queries for this category */
prompts: string[];
}
export const CHAT_EXAMPLE_CATEGORIES: ChatExampleCategory[] = [
{
id: "search",
label: "Search & Summarize",
prompts: [
"Summarize the key points across all the documents in this space.",
"What do my files say about [topic]? Answer with citations.",
"Find every mention of [keyword] and list the sources.",
"Give me a cited briefing on the documents I added this week.",
"Compare these two documents and highlight the differences.",
],
},
{
id: "create",
label: "Create",
prompts: [
"Write a cited research report on [topic] from my documents.",
"Turn this folder into a two-host podcast I can listen to.",
"Create a slide deck and a narrated video overview from these sources.",
"Generate an image to illustrate [concept] for my report.",
"Tailor my resume to this job description so it gets past ATS and lands an interview.",
],
},
{
id: "automate",
label: "Automate",
prompts: [
"Email me a daily brief of new documents in my knowledge base every morning.",
"When a PDF lands in my Research folder, generate a cited AI summary.",
"Generate a weekly status report from my Slack and Gmail every Friday.",
"Build an automation that turns new meeting notes into minutes with action items.",
"Run a monthly competitor analysis report and save it to my workspace.",
],
},
{
id: "tools",
label: "Across your tools",
prompts: [
"Search across my Notion, Slack, Google Drive and Gmail for [topic].",
"Post this research summary to my Notion workspace.",
"Send these meeting action items to our team Slack channel.",
"Create a Jira ticket from this bug report.",
"Open a Linear issue from this feature request.",
],
},
];

View file

@ -1,6 +1,6 @@
{ {
"name": "surfsense_web", "name": "surfsense_web",
"version": "0.0.25", "version": "0.0.26",
"private": true, "private": true,
"packageManager": "pnpm@10.26.0", "packageManager": "pnpm@10.26.0",
"description": "SurfSense Frontend", "description": "SurfSense Frontend",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB