matt HOFFNER commited on
Commit
c2df9c2
β€’
1 Parent(s): 366af2b
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+ .yarn/install-state.gz
8
+
9
+ # testing
10
+ /coverage
11
+
12
+ # next.js
13
+ /.next/
14
+ /out/
15
+
16
+ # production
17
+ /build
18
+
19
+ # misc
20
+ .DS_Store
21
+ *.pem
22
+
23
+ # debug
24
+ npm-debug.log*
25
+ yarn-debug.log*
26
+ yarn-error.log*
27
+
28
+ # local env files
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18 AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+
6
+ WORKDIR /app
7
+
8
+ # Install dependencies based on the preferred package manager
9
+ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
10
+ RUN \
11
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
12
+ elif [ -f package-lock.json ]; then npm ci; \
13
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
14
+ else echo "Lockfile not found." && exit 1; \
15
+ fi
16
+
17
+ # Uncomment the following lines if you want to use a secret at buildtime,
18
+ # for example to access your private npm packages
19
+ # RUN --mount=type=secret,id=HF_EXAMPLE_SECRET,mode=0444,required=true \
20
+ # $(cat /run/secrets/HF_EXAMPLE_SECRET)
21
+
22
+ # Rebuild the source code only when needed
23
+ FROM base AS builder
24
+ WORKDIR /app
25
+ COPY --from=deps /app/node_modules ./node_modules
26
+ COPY . .
27
+
28
+ # Next.js collects completely anonymous telemetry data about general usage.
29
+ # Learn more here: https://nextjs.org/telemetry
30
+ # Uncomment the following line in case you want to disable telemetry during the build.
31
+ # ENV NEXT_TELEMETRY_DISABLED 1
32
+
33
+ # RUN yarn build
34
+
35
+ # If you use yarn, comment out this line and use the line above
36
+ RUN npm run build
37
+
38
+ # Production image, copy all the files and run next
39
+ FROM base AS runner
40
+ WORKDIR /app
41
+
42
+ ENV NODE_ENV production
43
+ # Uncomment the following line in case you want to disable telemetry during runtime.
44
+ # ENV NEXT_TELEMETRY_DISABLED 1
45
+
46
+ RUN addgroup --system --gid 1001 nodejs
47
+ RUN adduser --system --uid 1001 nextjs
48
+
49
+ COPY --from=builder /app/public ./public
50
+
51
+ # Automatically leverage output traces to reduce image size
52
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
53
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
54
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
55
+
56
+ USER nextjs
57
+
58
+ EXPOSE 3000
59
+
60
+ ENV PORT 3000
61
+
62
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,23 @@
1
  ---
2
- title: Url Surfer
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: url-surfer
3
+ emoji: πŸ”—πŸ„β€β™‚οΈ
 
 
4
  sdk: docker
5
+ app_port: 3000
6
  ---
7
 
8
+ # πŸ”— url-surfer πŸ„β€β™‚οΈ
9
+
10
+ Simple API to navigate to a URL from a prompt, returning relevant context from the prompt using a vector store.
11
+
12
+ ## How it works
13
+
14
+ 1. Navigate (parse url/fetch/mime-type check)
15
+ 2. Extract text (jsdom/puppeteer/pdf-parse)
16
+ 3. Create vector store (transformers.js)
17
+ 4. Return vector results in prompt (langchain)
18
+
19
+
20
+ ## Ideas
21
+
22
+ * Configuration: Vector search size, token counting
23
+ * OpenAI functions integration
app/api/chat/route.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Configuration, OpenAIApi } from "openai-edge";
2
+ import { OpenAIStream, StreamingTextResponse } from "ai";
3
+ import { createUrlSurfer } from "@/app/tools/surfer";
4
+
5
+ const [, requestSchema] = createUrlSurfer();
6
+
7
+ const config = new Configuration({
8
+ apiKey: process.env.OPENAI_API_KEY,
9
+ });
10
+ const openai = new OpenAIApi(config);
11
+
12
+ const functions: any[] = [
13
+ requestSchema
14
+ ];
15
+
16
+ export async function POST(req: Request) {
17
+ const { messages, function_call } = await req.json()
18
+
19
+ const response = await openai.createChatCompletion({
20
+ model: 'gpt-4',
21
+ stream: true,
22
+ messages,
23
+ functions,
24
+ function_call
25
+ })
26
+
27
+ const stream = OpenAIStream(response)
28
+ return new StreamingTextResponse(stream)
29
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --max-width: 1100px;
3
+ --border-radius: 12px;
4
+ --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5
+ 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6
+ 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7
+
8
+ --foreground-rgb: 0, 0, 0;
9
+ --background-start-rgb: 214, 219, 220;
10
+ --background-end-rgb: 255, 255, 255;
11
+
12
+ --primary-glow: conic-gradient(
13
+ from 180deg at 50% 50%,
14
+ #16abff33 0deg,
15
+ #0885ff33 55deg,
16
+ #54d6ff33 120deg,
17
+ #0071ff33 160deg,
18
+ transparent 360deg
19
+ );
20
+ --secondary-glow: radial-gradient(
21
+ rgba(255, 255, 255, 1),
22
+ rgba(255, 255, 255, 0)
23
+ );
24
+
25
+ --tile-start-rgb: 239, 245, 249;
26
+ --tile-end-rgb: 228, 232, 233;
27
+ --tile-border: conic-gradient(
28
+ #00000080,
29
+ #00000040,
30
+ #00000030,
31
+ #00000020,
32
+ #00000010,
33
+ #00000010,
34
+ #00000080
35
+ );
36
+
37
+ --callout-rgb: 238, 240, 241;
38
+ --callout-border-rgb: 172, 175, 176;
39
+ --card-rgb: 180, 185, 188;
40
+ --card-border-rgb: 131, 134, 135;
41
+ }
42
+
43
+ @media (prefers-color-scheme: dark) {
44
+ :root {
45
+ --foreground-rgb: 255, 255, 255;
46
+ --background-start-rgb: 0, 0, 0;
47
+ --background-end-rgb: 0, 0, 0;
48
+
49
+ --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50
+ --secondary-glow: linear-gradient(
51
+ to bottom right,
52
+ rgba(1, 65, 255, 0),
53
+ rgba(1, 65, 255, 0),
54
+ rgba(1, 65, 255, 0.3)
55
+ );
56
+
57
+ --tile-start-rgb: 2, 13, 46;
58
+ --tile-end-rgb: 2, 5, 19;
59
+ --tile-border: conic-gradient(
60
+ #ffffff80,
61
+ #ffffff40,
62
+ #ffffff30,
63
+ #ffffff20,
64
+ #ffffff10,
65
+ #ffffff10,
66
+ #ffffff80
67
+ );
68
+
69
+ --callout-rgb: 20, 20, 20;
70
+ --callout-border-rgb: 108, 108, 108;
71
+ --card-rgb: 100, 100, 100;
72
+ --card-border-rgb: 200, 200, 200;
73
+ }
74
+ }
75
+
76
+ * {
77
+ box-sizing: border-box;
78
+ padding: 0;
79
+ margin: 0;
80
+ }
81
+
82
+ html,
83
+ body {
84
+ max-width: 100vw;
85
+ overflow-x: hidden;
86
+ }
87
+
88
+ body {
89
+ color: rgb(var(--foreground-rgb));
90
+ background: linear-gradient(
91
+ to bottom,
92
+ transparent,
93
+ rgb(var(--background-end-rgb))
94
+ )
95
+ rgb(var(--background-start-rgb));
96
+ }
97
+
98
+ a {
99
+ color: inherit;
100
+ text-decoration: none;
101
+ }
102
+
103
+ @media (prefers-color-scheme: dark) {
104
+ html {
105
+ color-scheme: dark;
106
+ }
107
+ }
app/icons.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export const FunctionIcon = ({ className }: { className?: string }) => {
3
+ return (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ fill="none"
7
+ viewBox="0 0 24 24"
8
+ width={25}
9
+ height={25}
10
+ strokeWidth={1.8}
11
+ stroke="currentColor"
12
+ className={className}
13
+ >
14
+ <path
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ d="M4.745 3A23.933 23.933 0 003 12c0 3.183.62 6.22 1.745 9M19.5 3c.967 2.78 1.5 5.817 1.5 9s-.533 6.22-1.5 9M8.25 8.885l1.444-.89a.75.75 0 011.105.402l2.402 7.206a.75.75 0 001.104.401l1.445-.889m-8.25.75l.213.09a1.687 1.687 0 002.062-.617l4.45-6.676a1.688 1.688 0 012.062-.618l.213.09"
18
+ />
19
+ </svg>
20
+ );
21
+ };
app/layout.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+
5
+ const inter = Inter({ subsets: ['latin'] })
6
+
7
+ export const metadata: Metadata = {
8
+ title: 'πŸ”— URL Surfer πŸ„β€β™‚οΈ',
9
+ description: 'Navigate to URLs and perform realtime similarity search',
10
+ }
11
+
12
+ export default function RootLayout({
13
+ children,
14
+ }: {
15
+ children: React.ReactNode
16
+ }) {
17
+ return (
18
+ <html lang="en">
19
+ <body className={inter.className}>{children}</body>
20
+ </html>
21
+ )
22
+ }
app/page.module.css ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ .main {
4
+ display: flex;
5
+ flex-direction: column;
6
+ justify-content: space-between;
7
+ padding: 6rem;
8
+ }
9
+
10
+ .description {
11
+ display: inherit;
12
+ justify-content: inherit;
13
+ align-items: inherit;
14
+ font-size: 0.85rem;
15
+ max-width: var(--max-width);
16
+ width: 100%;
17
+ z-index: 2;
18
+ font-family: var(--font-mono);
19
+ }
20
+
21
+ .description a {
22
+ display: flex;
23
+ justify-content: center;
24
+ align-items: center;
25
+ gap: 0.5rem;
26
+ }
27
+
28
+ .description p {
29
+ position: relative;
30
+ margin: 0;
31
+ padding: 1rem;
32
+ background-color: rgba(var(--callout-rgb), 0.5);
33
+ border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34
+ border-radius: var(--border-radius);
35
+ }
36
+
37
+ .code {
38
+ font-weight: 700;
39
+ font-family: var(--font-mono);
40
+ }
41
+
42
+ .grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(4, minmax(25%, auto));
45
+ max-width: 100%;
46
+ width: var(--max-width);
47
+ }
48
+
49
+ .card {
50
+ padding: 1rem 1.2rem;
51
+ border-radius: var(--border-radius);
52
+ background: rgba(var(--card-rgb), 0);
53
+ border: 1px solid rgba(var(--card-border-rgb), 0);
54
+ transition: background 200ms, border 200ms;
55
+ }
56
+
57
+ .card span {
58
+ display: inline-block;
59
+ transition: transform 200ms;
60
+ }
61
+
62
+ .card h2 {
63
+ font-weight: 600;
64
+ margin-bottom: 0.7rem;
65
+ }
66
+
67
+ .card p {
68
+ margin: 0;
69
+ opacity: 0.6;
70
+ font-size: 0.9rem;
71
+ line-height: 1.5;
72
+ max-width: 30ch;
73
+ }
74
+
75
+ .center {
76
+ display: flex;
77
+ justify-content: center;
78
+ align-items: center;
79
+ position: relative;
80
+ padding: 4rem 0;
81
+ }
82
+
83
+ .center::before {
84
+ background: var(--secondary-glow);
85
+ border-radius: 50%;
86
+ width: 480px;
87
+ height: 360px;
88
+ margin-left: -400px;
89
+ }
90
+
91
+ .center::after {
92
+ background: var(--primary-glow);
93
+ width: 240px;
94
+ height: 180px;
95
+ z-index: -1;
96
+ }
97
+
98
+ .center::before,
99
+ .center::after {
100
+ content: '';
101
+ left: 50%;
102
+ position: absolute;
103
+ filter: blur(45px);
104
+ transform: translateZ(0);
105
+ }
106
+
107
+ .logo {
108
+ position: relative;
109
+ }
110
+ /* Enable hover only on non-touch devices */
111
+ @media (hover: hover) and (pointer: fine) {
112
+ .card:hover {
113
+ background: rgba(var(--card-rgb), 0.1);
114
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
115
+ }
116
+
117
+ .card:hover span {
118
+ transform: translateX(4px);
119
+ }
120
+ }
121
+
122
+ @media (prefers-reduced-motion) {
123
+ .card:hover span {
124
+ transform: none;
125
+ }
126
+ }
127
+
128
+ /* Mobile */
129
+ @media (max-width: 700px) {
130
+ .content {
131
+ padding: 4rem;
132
+ }
133
+
134
+ .grid {
135
+ grid-template-columns: 1fr;
136
+ margin-bottom: 120px;
137
+ max-width: 320px;
138
+ text-align: center;
139
+ }
140
+
141
+ .card {
142
+ padding: 1rem 2.5rem;
143
+ }
144
+
145
+ .card h2 {
146
+ margin-bottom: 0.5rem;
147
+ }
148
+
149
+ .center {
150
+ padding: 8rem 0 6rem;
151
+ }
152
+
153
+ .center::before {
154
+ transform: none;
155
+ height: 300px;
156
+ }
157
+
158
+ .description {
159
+ font-size: 0.8rem;
160
+ }
161
+
162
+ .description a {
163
+ padding: 1rem;
164
+ }
165
+
166
+ .description p,
167
+ .description div {
168
+ display: flex;
169
+ justify-content: center;
170
+ position: fixed;
171
+ width: 100%;
172
+ }
173
+
174
+ .description p {
175
+ align-items: center;
176
+ inset: 0 0 auto;
177
+ padding: 2rem 1rem 1.4rem;
178
+ border-radius: 0;
179
+ border: none;
180
+ border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
181
+ background: linear-gradient(
182
+ to bottom,
183
+ rgba(var(--background-start-rgb), 1),
184
+ rgba(var(--callout-rgb), 0.5)
185
+ );
186
+ background-clip: padding-box;
187
+ backdrop-filter: blur(24px);
188
+ }
189
+
190
+ .description div {
191
+ align-items: flex-end;
192
+ pointer-events: none;
193
+ inset: auto 0 0;
194
+ padding: 2rem;
195
+ height: 200px;
196
+ background: linear-gradient(
197
+ to bottom,
198
+ transparent 0%,
199
+ rgb(var(--background-end-rgb)) 40%
200
+ );
201
+ z-index: 1;
202
+ }
203
+ }
204
+
205
+ /* Tablet and Smaller Desktop */
206
+ @media (min-width: 701px) and (max-width: 1120px) {
207
+ .grid {
208
+ grid-template-columns: repeat(2, 50%);
209
+ }
210
+ }
211
+
212
+ @media (prefers-color-scheme: dark) {
213
+ .vercelLogo {
214
+ filter: invert(1);
215
+ }
216
+
217
+ .logo {
218
+ filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
219
+ }
220
+ }
221
+
222
+ @keyframes rotate {
223
+ from {
224
+ transform: rotate(360deg);
225
+ }
226
+ to {
227
+ transform: rotate(0deg);
228
+ }
229
+ }
230
+
231
+ .response pre {
232
+ white-space: pre-wrap;
233
+ word-break: break-word;
234
+ overflow-wrap: break-word;
235
+ overflow-y: auto;
236
+ max-height: 500px; /* Maximum height before scrolling */
237
+ }
238
+
239
+ .input {
240
+ padding: 0.5rem 1rem; /* Adjust the padding inside the input */
241
+ margin: 0.5rem 0; /* Adds margin around the input for spacing */
242
+ border: 1px solid #ccc; /* A light border for the input */
243
+ border-radius: 4px; /* Rounded corners */
244
+ font-size: 1rem; /* Base font size */
245
+ line-height: 1.5; /* Height of the input line */
246
+ color: #333; /* Text color */
247
+ background-color: #fff; /* Background color of the input */
248
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: Adds a subtle shadow */
249
+ width: 100%; /* Make input full width of the parent container */
250
+ box-sizing: border-box; /* Include padding and border in the element's total width and height */
251
+ transition: border-color 0.3s, box-shadow 0.3s; /* Smooth transition for focus effect */
252
+ }
253
+
254
+ .input:focus {
255
+ border-color: #007bff; /* Highlight color when input is focused */
256
+ outline: none; /* Removes the default focus outline */
257
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); /* Adds a glow effect on focus */
258
+ }
259
+
260
+ .button {
261
+ padding: 0.75rem 1.5rem; /* Adjust padding to increase the button's size */
262
+ margin-top: 1rem; /* Space above the button */
263
+ border: none; /* No border for a button */
264
+ border-radius: 25px; /* Rounded corners for a pill shape */
265
+ font-size: 1rem; /* Font size */
266
+ font-weight: bold; /* Make the text a bit bolder */
267
+ color: white; /* Text color */
268
+ background-image: linear-gradient(to right, #6ee7b7, #3b82f6); /* Gradient background */
269
+ cursor: pointer; /* Change cursor to indicate it's clickable */
270
+ transition: transform 0.2s, background-color 0.3s; /* Smooth transitions for interactions */
271
+ text-transform: uppercase; /* Optional: uppercase text */
272
+ letter-spacing: 0.05em; /* Spacing between letters */
273
+ box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08); /* Subtle shadow for depth */
274
+ }
275
+
276
+ .button:hover, .button:focus {
277
+ background-image: linear-gradient(to left, #6ee7b7, #3b82f6); /* Change the direction of gradient on hover/focus */
278
+ transform: translateY(-2px); /* Slightly raise the button */
279
+ box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08); /* Increase shadow size */
280
+ }
281
+
282
+ .button:active {
283
+ transform: translateY(0); /* Button goes back down when clicked */
284
+ box-shadow: 0 3px 6px rgba(50, 50, 93, 0.16), 0 2px 4px rgba(0, 0, 0, 0.1); /* Smaller shadow when button is pressed */
285
+ }
286
+
287
+ .button:disabled {
288
+ background-image: linear-gradient(to right, #cbd5e1, #94a3b8); /* Less vibrant gradient for disabled state */
289
+ cursor: default; /* No pointer cursor since it's not clickable */
290
+ box-shadow: none; /* No shadow for a flat appearance */
291
+ }
292
+ .main {
293
+ display: flex;
294
+ flex-direction: column;
295
+ }
296
+
297
+ .title {
298
+ color: #333;
299
+ text-align: center;
300
+ margin-bottom: 2rem;
301
+ }
302
+
303
+ .messages {
304
+ flex: 1;
305
+ min-height: 80vh;
306
+ }
307
+
308
+ /* Chat message styles */
309
+ .message {
310
+ background-color: #f9f9f9;
311
+ border-radius: 15px;
312
+ padding: 10px 15px;
313
+ margin: 8px 0;
314
+ max-width: 80%;
315
+ word-break: break-word; /* Ensures text breaks nicely */
316
+ }
317
+
318
+ /* Different background for messages from the 'user' role */
319
+ .message-user {
320
+ background-color: #dcf8c6 !important;
321
+ }
322
+
323
+ /* Avatar styles */
324
+ .avatar {
325
+ width: 40px;
326
+ height: 40px;
327
+ border-radius: 50%;
328
+ background-color: #ccc;
329
+ margin-right: 10px;
330
+ }
331
+
332
+ /* Style for function call messages */
333
+ .function-call {
334
+ color: #555;
335
+ font-style: italic;
336
+ }
337
+
338
+ /* Input field styles */
339
+ .input {
340
+ padding: 10px;
341
+ margin: 10px 0;
342
+ width: 100%;
343
+ box-sizing: border-box;
344
+ border: 1px solid #ddd;
345
+ border-radius: 4px;
346
+ }
347
+
348
+ /* Button styles */
349
+ .button {
350
+ padding: 10px 20px;
351
+ background-color: #5cb85c;
352
+ color: white;
353
+ border: none;
354
+ border-radius: 4px;
355
+ cursor: pointer;
356
+ }
357
+
358
+ .button:disabled {
359
+ background-color: #ccc;
360
+ }
361
+
362
+ /* Form styles */
363
+ .form {
364
+ width: 100%;
365
+ max-width: 500px;
366
+ }
367
+
368
+ .flex {
369
+ display: flex;
370
+ align-items: flex-start;
371
+ }
372
+
373
+ .text-gray-500 {
374
+ color: #737373;
375
+ }
376
+
377
+ .font-bold {
378
+ font-weight: bold;
379
+ }
380
+
381
+ /* Additional media query for responsive design */
382
+ @media (max-width: 640px) {
383
+ .message {
384
+ max-width: 100%;
385
+ }
386
+ }
387
+
388
+ /* In your CSS file */
389
+ .avatar-user {
390
+ width: 20px; /* Based on Tailwind's width setting */
391
+ }
392
+
393
+ .bg-white {
394
+ background-color: white;
395
+ }
396
+
397
+ .bg-black {
398
+ background-color: black;
399
+ }
400
+
401
+ .bg-gray-100 {
402
+ background-color: #f7fafc; /* Approximation of Tailwind's gray-100 */
403
+ }
404
+
405
+ .bg-green-500 {
406
+ background-color: #48bb78; /* Approximation of Tailwind's green-500 */
407
+ }
408
+
409
+ .bg-gray-200 {
410
+ background-color: #edf2f7; /* Approximation of Tailwind's gray-200 */
411
+ }
412
+
413
+ .bg-blue-500 {
414
+ background-color: #4299e1; /* Approximation of Tailwind's blue-500 */
415
+ }
app/page.tsx ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import styles from './page.module.css';
4
+ import { useState } from 'react';
5
+ import { useChat } from 'ai/react';
6
+ import { FunctionCallHandler, Message, nanoid } from 'ai';
7
+ import ReactMarkdown from "react-markdown";
8
+ import { Bot, User } from "lucide-react";
9
+ import { toast } from 'sonner';
10
+ import { FunctionIcon } from './icons';
11
+
12
+ const Page: React.FC = () => {
13
+ const functionCallHandler: FunctionCallHandler = async (
14
+ chatMessages,
15
+ functionCall,
16
+ ) => {
17
+ let result;
18
+ const response = await fetch("/api/surfer", {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ },
23
+ body: JSON.stringify({
24
+ prompt: input
25
+ })
26
+ } as any);
27
+
28
+ if (!response.ok) {
29
+ const errorText = await response.text();
30
+ toast.error(`Something went wrong: ${errorText}`);
31
+ return;
32
+ }
33
+
34
+ result = await response.text();
35
+
36
+ return {
37
+ messages: [
38
+ ...chatMessages,
39
+ {
40
+ id: nanoid(),
41
+ name: functionCall.name,
42
+ role: "function" as const,
43
+ content: result,
44
+ },
45
+ ],
46
+ };
47
+ };
48
+ const { messages, input, setInput, handleSubmit, isLoading } = useChat({
49
+ experimental_onFunctionCall: functionCallHandler,
50
+ onResponse: (response: { status: number; }) => {
51
+ if (response.status === 429) {
52
+ toast.error("You have reached your request limit for the day.");
53
+ return;
54
+ } else {
55
+ console.log("chat initialized");
56
+ }
57
+ },
58
+ onError: (error: any) => {
59
+ console.log(error);
60
+ },
61
+ });
62
+ const [isExpanded, setIsExpanded] = useState(false);
63
+ const toggleExpand = () => {
64
+ setIsExpanded(!isExpanded);
65
+ };
66
+
67
+ const roleUIConfig: {
68
+ [key: string]: {
69
+ avatar: JSX.Element;
70
+ bgColor: string;
71
+ avatarColor: string;
72
+ // eslint-disable-next-line no-unused-vars
73
+ dialogComponent: (message: Message) => JSX.Element;
74
+ };
75
+ } = {
76
+ user: {
77
+ avatar: <User width={20} />,
78
+ bgColor: "bg-white",
79
+ avatarColor: "bg-black",
80
+ dialogComponent: (message: Message) => (
81
+ <ReactMarkdown
82
+ className=""
83
+ components={{
84
+ a: (props) => (
85
+ <a {...props} target="_blank" rel="noopener noreferrer" />
86
+ ),
87
+ }}
88
+ >
89
+ {message.content}
90
+ </ReactMarkdown>
91
+ ),
92
+ },
93
+ assistant: {
94
+ avatar: <Bot width={20} />,
95
+ bgColor: "bg-gray-100",
96
+ avatarColor: "bg-green-500",
97
+ dialogComponent: (message: Message) => (
98
+ <ReactMarkdown
99
+ className=""
100
+ components={{
101
+ a: (props) => (
102
+ <a {...props} target="_blank" rel="noopener noreferrer" />
103
+ ),
104
+ }}
105
+ >
106
+ {message.content}
107
+ </ReactMarkdown>
108
+ ),
109
+ },
110
+ function: {
111
+ avatar: <div className="cursor-pointer" onClick={toggleExpand}><FunctionIcon /></div>,
112
+ bgColor: "bg-gray-200",
113
+ avatarColor: "bg-blue-500",
114
+ dialogComponent: (message: Message) => {
115
+ return (
116
+ <div className="flex flex-col">
117
+ {isExpanded && (
118
+ <div className="py-1">{message.content}</div>
119
+ )}
120
+ </div>
121
+ );
122
+ },
123
+ }
124
+ };
125
+
126
+
127
+ return (
128
+ <main className={styles.main}>
129
+ <h1 className={styles.title}>
130
+ πŸ”— URL Surfer πŸ„β€β™‚οΈ
131
+ </h1>
132
+ <div className={styles.messages}>
133
+ {messages.length > 0 ? (
134
+ messages.map((message, i) => {
135
+ const messageClass = `message ${message.role === 'user' ? 'message-user' : ''}`;
136
+ return (
137
+ <div key={i} className={messageClass}>
138
+ <div className="flex w-full max-w-screen-md items-start space-x-4 px-5 sm:px-0">
139
+ <div className="avatar">
140
+ {roleUIConfig[message.role].avatar}
141
+ </div>
142
+ {message.content === "" && message.function_call != undefined ? (
143
+ typeof message.function_call === "object" ? (
144
+ <div className="flex flex-col">
145
+ <div>
146
+ Using{" "}
147
+ <span className="font-bold">
148
+ {message.function_call.name}
149
+ </span>{" "}
150
+ ...
151
+ </div>
152
+ <div className="">
153
+ {message.function_call.arguments}
154
+ </div>
155
+ </div>
156
+ ) : (
157
+ <div className="function-call">{message.function_call}</div>
158
+ )
159
+ ) : (
160
+ roleUIConfig[message.role].dialogComponent(message)
161
+ )}
162
+ </div>
163
+ </div>
164
+ );
165
+ })) : null}
166
+ </div>
167
+ <form onSubmit={handleSubmit} className={styles.form}>
168
+ <input
169
+ type="text"
170
+ value={input}
171
+ onChange={(e) => setInput(e.target.value)}
172
+ placeholder="Enter URL and Prompt"
173
+ className={styles.input}
174
+ />
175
+ <button type="submit" className={styles.button} disabled={isLoading}>
176
+ {isLoading ? 'Loading...' : 'Submit'}
177
+ </button>
178
+ </form>
179
+ </main>
180
+ );
181
+ };
182
+
183
+ export default Page;
app/tools/surfer.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Tool } from 'openai-function-calling-tools';
2
+ import { z } from 'zod';
3
+
4
+ function createUrlSurfer() {
5
+ const paramsSchema = z.object({
6
+ input: z.string(),
7
+ });
8
+ const name = 'surfer';
9
+ const description = 'A custom URL navigator. Useful when a URL is provided with a question. Input should be a prompt with a URL. Outputs a JSON array of relevant results.';
10
+
11
+ const execute = async ({ input }: z.infer<typeof paramsSchema>) => {
12
+ try {
13
+ const res = await fetch('/api/surfer', {
14
+ method: 'POST',
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ body: JSON.stringify({ prompt: input }),
19
+ });
20
+
21
+ if (!res.ok) {
22
+ throw new Error(`HTTP error! status: ${res.status}`);
23
+ }
24
+
25
+ const data = await res.json();
26
+ return data;
27
+ } catch (error) {
28
+ // @ts-ignore
29
+ throw new Error(`Error in UrlSurfer: ${error.message}`);
30
+ }
31
+ };
32
+
33
+ return new Tool(paramsSchema, name, description, execute).tool;
34
+ }
35
+
36
+ export { createUrlSurfer };
next.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: "standalone"
4
+ }
5
+
6
+ module.exports = nextConfig
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "url-surfer",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@emotion/react": "^11.11.1",
13
+ "@emotion/styled": "^11.11.0",
14
+ "@mui/material": "^5.14.16",
15
+ "@upstash/ratelimit": "^0.4.4",
16
+ "@vercel/kv": "^0.2.4",
17
+ "@xenova/transformers": "^2.7.0",
18
+ "ai": "^2.2.20",
19
+ "jsdom": "^22.1.0",
20
+ "langchain": "^0.0.180",
21
+ "lucide-react": "^0.292.0",
22
+ "next": "14.0.1",
23
+ "node-fetch": "^3.3.2",
24
+ "openai": "^3.3.0",
25
+ "openai-edge": "^1.2.2",
26
+ "openai-function-calling-tools": "^6.1.3",
27
+ "pdf-parse": "^1.1.1",
28
+ "puppeteer": "^19.11.1",
29
+ "react": "^18",
30
+ "react-dom": "^18",
31
+ "react-markdown": "^9.0.0",
32
+ "sonner": "^1.1.0",
33
+ "zod": "^3.22.4",
34
+ "zod-to-json-schema": "^3.21.4"
35
+ },
36
+ "devDependencies": {
37
+ "@types/jsdom": "^21.1.4",
38
+ "@types/node": "^20",
39
+ "@types/pdf-parse": "^1.1.3",
40
+ "@types/puppeteer": "^7.0.4",
41
+ "@types/react": "^18",
42
+ "@types/react-dom": "^18",
43
+ "eslint": "^8",
44
+ "eslint-config-next": "14.0.1",
45
+ "typescript": "^5"
46
+ }
47
+ }
pages/api/surfer/index.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextApiRequest, NextApiResponse } from 'next';
2
+ import fetch from 'node-fetch';
3
+ import { JSDOM } from 'jsdom';
4
+ // @ts-ignore
5
+ import pdfParse from 'pdf-parse';
6
+ import puppeteer from 'puppeteer';
7
+ import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
8
+ import { MemoryVectorStore } from 'langchain/vectorstores/memory';
9
+ import { HuggingFaceTransformersEmbeddings } from "langchain/embeddings/hf_transformers";
10
+
11
+ export const config = {
12
+ api: {
13
+ bodyParser: {
14
+ sizeLimit: '1mb',
15
+ },
16
+ },
17
+ };
18
+
19
+ const DEFAULT_CHUNK_SIZE = 1000;
20
+ const VECTOR_STORE_SIZE = 10;
21
+ const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: DEFAULT_CHUNK_SIZE });
22
+
23
+ async function extractTextFromPDF(buffer: Buffer): Promise<string> {
24
+ const data = await pdfParse(buffer);
25
+ return data.text;
26
+ }
27
+
28
+ const model = new HuggingFaceTransformersEmbeddings({
29
+ modelName: "Xenova/all-MiniLM-L6-v2",
30
+ });
31
+
32
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
33
+
34
+ const handleContentText = async (targetUrl: string) => {
35
+ const response = await fetch(targetUrl);
36
+ const contentType = response.headers.get('content-type') || '';
37
+ let content;
38
+ if (contentType.includes('application/pdf')) {
39
+ const buffer = await response.arrayBuffer();
40
+ content = await extractTextFromPDF(buffer as any);
41
+ } else if (contentType.includes('text/html')) {
42
+ const html = await response.text();
43
+ const dom = new JSDOM(html);
44
+ const scripts = dom.window.document.querySelectorAll('script, style');
45
+ scripts.forEach(element => element.remove());
46
+ content = dom.window.document.body.textContent || '';
47
+
48
+ if (!content.trim()) {
49
+ const browser = await puppeteer.launch();
50
+ const page = await browser.newPage();
51
+ await page.goto(targetUrl);
52
+ content = await page.evaluate(() => document.body.innerText);
53
+ await browser.close();
54
+ }
55
+ } else {
56
+ content = await response.text();
57
+ }
58
+ return content;
59
+ }
60
+
61
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
62
+ const prompt = req.body.prompt as string;
63
+ const urls = prompt.match(urlRegex);
64
+ const targetUrl = urls ? urls[0] : null;
65
+ const promptWithoutUrl = urls ? prompt.replace(urlRegex, '').trim() : prompt;
66
+
67
+ if (!targetUrl) {
68
+ return `Couldn't find url, here is the ${prompt}`;
69
+ }
70
+
71
+ try {
72
+ const content: string = await handleContentText(targetUrl)
73
+ if (!content) {
74
+ return `Couldn't find ${targetUrl}, here is the prompt: ${promptWithoutUrl}`;
75
+ }
76
+
77
+ const documents = await textSplitter.createDocuments([content]);
78
+
79
+ const vectorStore = await MemoryVectorStore.fromTexts(
80
+ // @ts-ignore
81
+ [...documents.map(doc => doc.pageContent)],
82
+ // @ts-ignore
83
+ [...documents.map((v, k) => k)],
84
+ model
85
+ )
86
+ const queryResult = await vectorStore.similaritySearch(promptWithoutUrl, VECTOR_STORE_SIZE);
87
+ return res.status(200).send(
88
+ `Here is the context: ${JSON.stringify(queryResult.map(result => result.pageContent))} from using the prompt to lookup relevant information. Here is the prompt: ${promptWithoutUrl}`);
89
+ } catch (error) {
90
+ console.error(error);
91
+ // @ts-ignore
92
+ return res.status(500).json({ error: error.message });
93
+ }
94
+ }
public/next.svg ADDED
public/vercel.svg ADDED
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }