diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 00000000..b1a5dde5 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,5 @@ +# OpenAI API key retrieved here: https://platform.openai.com/api-keys +OPENAI_API_KEY= + +# Tavily API Key retrieved here: https://app.tavily.com/home +TAVILY_API_KEY= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..89d1965f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..2b92e157 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Morphic Search + +An AI-powered answer engine with a generative UI. + +![capture](/public/capture-240404.png) + +## 🔍 Overview + +- 🧱 [Stack](#-stack) +- 🚀 [Quickstart](#-quickstart) +- 🌐 [Deploy](#-deploy) + +## 🧱 Stack + +- App framework: [Next.js](https://nextjs.org/) +- Text streaming / Generative UI: [Vercel AI SDK](https://sdk.vercel.ai/docs) +- Generative Model: [OpenAI](https://openai.com/) +- Search API: [Tavily AI](https://tavily.com/) +- CSS framework: [Tailwind CSS](https://tailwindcss.com/) +- Component library: [shadcn/ui](https://ui.shadcn.com/) + +## 🚀 Quickstart + +### 1. Fork and Clone repo + +Fork the repo to your Github account, then run the following command to clone the repo: + +``` +git clone git@github.com:[YOUR_GITHUB_ACCOUNT]/morphic.git +``` + +### 2. Install dependencies + +``` +cd morphic +bun i +``` + +### 3. Fill out secrets + +``` +cp .env.local.example .env.local +``` + +Your .env.local file should look like this: + +``` +# OpenAI API key retrieved here: https://platform.openai.com/api-keys +OPENAI_API_KEY=[YOUR_OPENAI_API_KEY] + +# Tavily API Key retrieved here: https://app.tavily.com/home +TAVILY_API_KEY=[YOUR_TAVILY_API_KEY] +``` + +### 4. Run app locally + +``` +bun dev +``` + +You can now visit http://localhost:3000. + +## 🌐 Deploy + +Host your own live version of Morphic with Vercel. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmiurla%2Fmorphic&env=OPENAI_API_KEY,TAVILY_API_KEY) diff --git a/app/action.tsx b/app/action.tsx new file mode 100644 index 00000000..9428c37b --- /dev/null +++ b/app/action.tsx @@ -0,0 +1,117 @@ +import { + StreamableValue, + createAI, + createStreamableUI, + createStreamableValue, + getMutableAIState +} from 'ai/rsc' +import { ExperimentalMessage } from 'ai' +import { Spinner } from '@/components/ui/spinner' +import { Section } from '@/components/section' +import { FollowupPanel } from '@/components/followup-panel' +import { inquire, researcher, taskManager, querySuggestor } from '@/app/agents' + +async function submit(formData?: FormData, skip?: boolean) { + 'use server' + + const aiState = getMutableAIState() + const uiStream = createStreamableUI() + const isGenerating = createStreamableValue(true) + + const messages: ExperimentalMessage[] = aiState.get() as any + // Get the user input from the form data + const userInput = skip + ? `{"action": "skip"}` + : (formData?.get('input') as string) + const content = skip + ? userInput + : formData + ? JSON.stringify(Object.fromEntries(formData)) + : null + // Add the user message to the state + if (content) { + const message = { role: 'user', content } + console.log('Message content', content, aiState.get()) + messages.push(message as ExperimentalMessage) + aiState.update([...(aiState.get() as any), message]) + } + + async function processEvents() { + uiStream.update() + + let action: any = { object: { next: 'proceed' } } + // If the user skips the task, we proceed to the search + if (!skip) action = await taskManager(messages) + console.log('Next Action: ', action) + + if (action.object.next === 'inquire') { + // Generate inquiry + const inquiry = await inquire(uiStream, messages, userInput) + + uiStream.done() + isGenerating.done() + aiState.done([ + ...aiState.get(), + { role: 'assistant', content: `inquiry: ${inquiry?.inquiry}` } + ]) + return + } + + // Generate the answer + let answer = '' + const streamText = createStreamableValue() + while (answer.length === 0) { + // Search the web and generate the answer + const { fullResponse } = await researcher(uiStream, streamText, messages) + answer = fullResponse + } + + // Generate related queries + await querySuggestor(uiStream, messages) + + // Add follow-up panel + uiStream.append( +
+ +
+ ) + + isGenerating.done(false) + uiStream.done() + aiState.done([...aiState.get(), { role: 'assistant', content: answer }]) + } + + processEvents() + + return { + id: Date.now(), + isGenerating: isGenerating.value, + component: uiStream.value + } +} + +// Define the initial state of the AI. It can be any JSON object. +const initialAIState: { + role: 'user' | 'assistant' | 'system' | 'function' | 'tool' + content: string + id?: string + name?: string +}[] = [] + +// The initial UI state that the client will keep track of, which contains the message IDs and their UI nodes. +const initialUIState: { + id: number + isGenerating: StreamableValue + component: React.ReactNode +}[] = [] + +// AI is a provider you wrap your application with so you can access AI and UI state in your components. +export const AI = createAI({ + actions: { + submit + }, + // Each state can be any shape of object, but for chat applications + // it makes sense to have an array of messages. Or you may prefer something like { id: number, messages: Message[] } + initialUIState, + initialAIState +}) diff --git a/app/agents/index.tsx b/app/agents/index.tsx new file mode 100644 index 00000000..e47c0492 --- /dev/null +++ b/app/agents/index.tsx @@ -0,0 +1,4 @@ +export * from './task-manager' +export * from './inquire' +export * from './query-suggestor' +export * from './researcher' diff --git a/app/agents/inquire.tsx b/app/agents/inquire.tsx new file mode 100644 index 00000000..9da75af5 --- /dev/null +++ b/app/agents/inquire.tsx @@ -0,0 +1,46 @@ +import { openai } from 'ai/openai' +import { Copilot } from '@/components/copilot' +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { ExperimentalMessage, experimental_streamObject } from 'ai' +import { PartialInquiry, inquirySchema } from '@/app/schema/inquiry' + +export async function inquire( + uiStream: ReturnType, + messages: ExperimentalMessage[], + query?: string +) { + const objectStream = createStreamableValue() + uiStream.update() + + let finalInquiry: PartialInquiry = {} + await experimental_streamObject({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `You are a professional web researcher tasked with deepening your understanding of the user's input through further inquiries. + Only ask additional questions if absolutely necessary after receiving an initial response from the user. + 'names' should be an array of English identifiers for the options provided. Structure your inquiry as follows: + e.g., { + "inquiry": "What specific information are you seeking about Rivian?", + "options": ["History", "Products", "Investors", "Partnerships", "Competitors"], + "names": ["history", "products", "investors", "partnerships", "competitors"], + "allowsInput": true, + "inputLabel": "If other, please specify", + "inputPlaceholder": "e.g., Specifications" + }`, + messages, + schema: inquirySchema + }) + .then(async result => { + for await (const obj of result.partialObjectStream) { + if (obj) { + objectStream.update(obj) + finalInquiry = obj + } + } + }) + .finally(() => { + objectStream.done() + }) + + return finalInquiry +} diff --git a/app/agents/query-suggestor.tsx b/app/agents/query-suggestor.tsx new file mode 100644 index 00000000..5c56705f --- /dev/null +++ b/app/agents/query-suggestor.tsx @@ -0,0 +1,43 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { ExperimentalMessage, experimental_streamObject } from 'ai' +import { PartialRelated, relatedSchema } from '@/app/schema/related' +import { Section } from '@/components/section' +import SearchRelated from '@/components/search-related' +import { openai } from 'ai/openai' + +export async function querySuggestor( + uiStream: ReturnType, + messages: ExperimentalMessage[] +) { + const objectStream = createStreamableValue() + uiStream.append( +
+ +
+ ) + + await experimental_streamObject({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `You are tasked as a professional web researcher to generate queries that delve deeper into the subject based on the initial query and its search results. Your goal is to formulate three related questions. + For example, given the query: "Starship's third test flight key milestones", + Your output should look like: + "{ + "related": [ + "Key milestones achieved during Starship's third test flight", + "Reason for Starship's failure during the third test flight", + "Future plans for Starship following the third test flight" + ] + }"`, + messages, + schema: relatedSchema + }) + .then(async result => { + for await (const obj of result.partialObjectStream) { + objectStream.update(obj) + } + }) + .finally(() => { + objectStream.done() + }) +} diff --git a/app/agents/researcher.tsx b/app/agents/researcher.tsx new file mode 100644 index 00000000..36c7b4a8 --- /dev/null +++ b/app/agents/researcher.tsx @@ -0,0 +1,157 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { + ExperimentalMessage, + ToolCallPart, + ToolResultPart, + experimental_streamText +} from 'ai' +import { searchSchema } from '@/app/schema/search' +import { Section } from '@/components/section' +import { openai } from 'ai/openai' +import { ToolBadge } from '@/components/tool-badge' +import { SearchSkeleton } from '@/components/search-skeleton' +import { SearchResults } from '@/components/search-results' +import { BotMessage } from '@/components/message' +import Exa from 'exa-js' +import { SearchResultsImageSection } from '@/components/search-results-image' + +export async function researcher( + uiStream: ReturnType, + streamText: ReturnType>, + messages: ExperimentalMessage[] +) { + const searchAPI: 'tavily' | 'exa' = 'tavily' + + let fullResponse = '' + const result = await experimental_streamText({ + model: openai.chat('gpt-4-turbo-preview'), + maxTokens: 2500, + system: `As a professional web researcher, you possess the ability to search the web for any information. + Utilize the search results to offer additional assistance or information as needed. + Include a key image in your response if one is relevant. Respond to the user's query accordingly. + `, + messages, + tools: { + search: { + description: 'Search the web for information', + parameters: searchSchema, + execute: async ({ + query, + max_results, + search_depth + }: { + query: string + max_results: number + search_depth: 'basic' | 'advanced' + }) => { + uiStream.update( +
+ {`${query}`} +
+ ) + + uiStream.append( +
+ +
+ ) + + const searchResult = + searchAPI === 'tavily' + ? await tavilySearch(query, max_results, search_depth) + : await exaSearch(query) + + uiStream.update( +
+ +
+ ) + uiStream.append( +
+ +
+ ) + + uiStream.append( +
+ +
+ ) + + return searchResult + } + } + } + }) + + const toolCalls: ToolCallPart[] = [] + const toolResponses: ToolResultPart[] = [] + for await (const delta of result.fullStream) { + switch (delta.type) { + case 'text-delta': + if (delta.textDelta) { + fullResponse += delta.textDelta + streamText.update(fullResponse) + } + break + case 'tool-call': + toolCalls.push(delta) + break + case 'tool-result': + toolResponses.push(delta) + break + } + } + messages.push({ + role: 'assistant', + content: [{ type: 'text', text: fullResponse }, ...toolCalls] + }) + + if (toolResponses.length > 0) { + // Add tool responses to the messages + messages.push({ role: 'tool', content: toolResponses }) + } + + return { result, fullResponse } +} + +async function tavilySearch( + query: string, + maxResults: number = 10, + searchDepth: 'basic' | 'advanced' = 'basic' +): Promise { + const apiKey = process.env.TAVILY_API_KEY + const response = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + api_key: apiKey, + query, + max_results: maxResults < 5 ? 5 : maxResults, + search_depth: searchDepth, + include_images: true, + include_answers: true + }) + }) + + if (!response.ok) { + throw new Error(`Error: ${response.status}`) + } + + const data = await response.json() + return data +} + +async function exaSearch(query: string, maxResults: number = 10): Promise { + const apiKey = process.env.EXA_API_KEY + const exa = new Exa(apiKey) + return exa.searchAndContents(query, { + highlights: true, + numResults: maxResults + }) +} diff --git a/app/agents/task-manager.tsx b/app/agents/task-manager.tsx new file mode 100644 index 00000000..4a506890 --- /dev/null +++ b/app/agents/task-manager.tsx @@ -0,0 +1,19 @@ +import { ExperimentalMessage, experimental_generateObject } from 'ai' +import { openai } from 'ai/openai' +import { nextActionSchema } from '../schema/next-action' + +// Decide whether inquiry is required for the user input +export async function taskManager(messages: ExperimentalMessage[]) { + const result = await experimental_generateObject({ + model: openai.chat('gpt-3.5-turbo'), + system: `You are a professional web researcher tasked with fully understanding the user's query, conducting web searches for the necessary information, and responding accordingly. + Your goal is to comprehend the user's input and determine the next step. You have the option to present a form and request further information from the user. + Select one of the three options based on the context: "proceed", "inquire". Use "proceed" if the provided information is sufficient for action. + Opt for "inquire" if more information can be gathered through questions, allowing for default selections and free-form input. + Make your choice wisely to fulfill your mission effectively.`, + messages, + schema: nextActionSchema + }) + + return result +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 00000000..2cee9d41 Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 00000000..e7f71728 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 96.1%; + --card-foreground: 0 0% 45.1%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 89.8%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 14.9%; + --card-foreground: 0 0% 63.9%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 14.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..0dc3dccc --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from 'next' +import { Inter as FontSans } from 'next/font/google' +import { AI } from './action' +import './globals.css' +import { cn } from '@/lib/utils' +import { ThemeProvider } from '@/components/theme-provider' +import Header from '@/components/header' +import Footer from '@/components/footer' + +const fontSans = FontSans({ + subsets: ['latin'], + variable: '--font-sans' +}) + +export const metadata: Metadata = { + title: 'Morphic Search - AI powered answer engine', + description: 'An AI-powered answer engine with a generative UI.' +} + +export default function RootLayout({ + children +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+ {children} +