diff --git a/.gitignore b/.gitignore index 4a7f73a..a6a5428 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ logs .env .env.* !.env.example + +# Scraping data +.venv +__pycache__ \ No newline at end of file diff --git a/bun.lock b/bun.lock index 907ca8c..0ea46d7 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@nuxtjs/tailwindcss": "6.14.0", "@tailwindcss/vite": "^4.1.5", "@uploadthing/nuxt": "^7.1.7", + "@vueuse/core": "^13.1.0", "animate.css": "^4.1.1", "bootstrap-icons": "^1.12.1", "class-variance-authority": "^0.7.1", @@ -614,11 +615,11 @@ "@vue/shared": ["@vue/shared@3.5.13", "", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="], - "@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="], + "@vueuse/core": ["@vueuse/core@13.1.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.1.0", "@vueuse/shared": "13.1.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q=="], - "@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="], + "@vueuse/metadata": ["@vueuse/metadata@13.1.0", "", {}, "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw=="], - "@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="], + "@vueuse/shared": ["@vueuse/shared@13.1.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew=="], "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], @@ -2454,8 +2455,6 @@ "nuxt/oxc-parser": ["oxc-parser@0.68.1", "", { "dependencies": { "@oxc-project/types": "^0.68.1" }, "optionalDependencies": { "@oxc-parser/binding-darwin-arm64": "0.68.1", "@oxc-parser/binding-darwin-x64": "0.68.1", "@oxc-parser/binding-linux-arm-gnueabihf": "0.68.1", "@oxc-parser/binding-linux-arm64-gnu": "0.68.1", "@oxc-parser/binding-linux-arm64-musl": "0.68.1", "@oxc-parser/binding-linux-x64-gnu": "0.68.1", "@oxc-parser/binding-linux-x64-musl": "0.68.1", "@oxc-parser/binding-wasm32-wasi": "0.68.1", "@oxc-parser/binding-win32-arm64-msvc": "0.68.1", "@oxc-parser/binding-win32-x64-msvc": "0.68.1" } }, "sha512-dHwz+xP9r1GTvqyywfws4j7EEP/OaeTpHEjTcvIjViB/R2IdUn52AnoUFNjpw8yRU52XVE76rOA4IEj7I0EjnA=="], - "nuxt-link-checker/@vueuse/core": ["@vueuse/core@13.1.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.1.0", "@vueuse/shared": "13.1.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q=="], - "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], @@ -2480,6 +2479,10 @@ "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "reka-ui/@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="], + + "reka-ui/@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="], + "replace-in-file/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "replace-in-file/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -2750,10 +2753,6 @@ "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "nuxt-link-checker/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@13.1.0", "", {}, "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw=="], - - "nuxt-link-checker/@vueuse/core/@vueuse/shared": ["@vueuse/shared@13.1.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew=="], - "nuxt/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.68.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y5FBQyPCLsldAZYEd+oZcUboXwpcLf42Lakx3EYtiYDbuK9M3IqBXMGxdM07P4PfGQrKYn6/cC8xAqkVHnbWPw=="], "nuxt/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.68.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-nkiXpEKl8UOhNPdOY5hA2PFq9vQc9xVs7NFu2vUD9eH/j5uYfv8GnNaKkd+v6iH93JwEBxuK5gfwxiiCEMZRyg=="], @@ -2778,6 +2777,8 @@ "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "reka-ui/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="], + "replace-in-file/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "resolve-path/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], diff --git a/components/ui/command/Command.vue b/components/ui/command/Command.vue new file mode 100644 index 0000000..9597010 --- /dev/null +++ b/components/ui/command/Command.vue @@ -0,0 +1,92 @@ + + + diff --git a/components/ui/command/CommandDialog.vue b/components/ui/command/CommandDialog.vue new file mode 100644 index 0000000..b7b0a91 --- /dev/null +++ b/components/ui/command/CommandDialog.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/ui/command/CommandEmpty.vue b/components/ui/command/CommandEmpty.vue new file mode 100644 index 0000000..58795f2 --- /dev/null +++ b/components/ui/command/CommandEmpty.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/ui/command/CommandGroup.vue b/components/ui/command/CommandGroup.vue new file mode 100644 index 0000000..fd4f5b3 --- /dev/null +++ b/components/ui/command/CommandGroup.vue @@ -0,0 +1,46 @@ + + + diff --git a/components/ui/command/CommandInput.vue b/components/ui/command/CommandInput.vue new file mode 100644 index 0000000..4c3b963 --- /dev/null +++ b/components/ui/command/CommandInput.vue @@ -0,0 +1,37 @@ + + + diff --git a/components/ui/command/CommandItem.vue b/components/ui/command/CommandItem.vue new file mode 100644 index 0000000..97365e1 --- /dev/null +++ b/components/ui/command/CommandItem.vue @@ -0,0 +1,78 @@ + + + diff --git a/components/ui/command/CommandList.vue b/components/ui/command/CommandList.vue new file mode 100644 index 0000000..83c7d95 --- /dev/null +++ b/components/ui/command/CommandList.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/ui/command/CommandSeparator.vue b/components/ui/command/CommandSeparator.vue new file mode 100644 index 0000000..4122020 --- /dev/null +++ b/components/ui/command/CommandSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/components/ui/command/CommandShortcut.vue b/components/ui/command/CommandShortcut.vue new file mode 100644 index 0000000..0d4da92 --- /dev/null +++ b/components/ui/command/CommandShortcut.vue @@ -0,0 +1,14 @@ + + + diff --git a/components/ui/command/index.ts b/components/ui/command/index.ts new file mode 100644 index 0000000..cb48e1e --- /dev/null +++ b/components/ui/command/index.ts @@ -0,0 +1,25 @@ +import type { Ref } from 'vue' +import { createContext } from 'reka-ui' + +export { default as Command } from './Command.vue' +export { default as CommandDialog } from './CommandDialog.vue' +export { default as CommandEmpty } from './CommandEmpty.vue' +export { default as CommandGroup } from './CommandGroup.vue' +export { default as CommandInput } from './CommandInput.vue' +export { default as CommandItem } from './CommandItem.vue' +export { default as CommandList } from './CommandList.vue' +export { default as CommandSeparator } from './CommandSeparator.vue' +export { default as CommandShortcut } from './CommandShortcut.vue' + +export const [useCommand, provideCommandContext] = createContext<{ + allItems: Ref> + allGroups: Ref>> + filterState: { + search: string + filtered: { count: number, items: Map, groups: Set } + } +}>('Command') + +export const [useCommandGroup, provideCommandGroupContext] = createContext<{ + id?: string +}>('CommandGroup') diff --git a/components/ui/dialog/Dialog.vue b/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..9fc9c7d --- /dev/null +++ b/components/ui/dialog/Dialog.vue @@ -0,0 +1,14 @@ + + + diff --git a/components/ui/dialog/DialogClose.vue b/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..ba036b5 --- /dev/null +++ b/components/ui/dialog/DialogClose.vue @@ -0,0 +1,11 @@ + + + diff --git a/components/ui/dialog/DialogContent.vue b/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..d84f271 --- /dev/null +++ b/components/ui/dialog/DialogContent.vue @@ -0,0 +1,50 @@ + + + diff --git a/components/ui/dialog/DialogDescription.vue b/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..5afbab0 --- /dev/null +++ b/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/ui/dialog/DialogFooter.vue b/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..ac2d0c1 --- /dev/null +++ b/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/ui/dialog/DialogHeader.vue b/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..b2c9085 --- /dev/null +++ b/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/components/ui/dialog/DialogScrollContent.vue b/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..ee6b5e2 --- /dev/null +++ b/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,59 @@ + + + diff --git a/components/ui/dialog/DialogTitle.vue b/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..30cbb36 --- /dev/null +++ b/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,29 @@ + + + diff --git a/components/ui/dialog/DialogTrigger.vue b/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..2984f37 --- /dev/null +++ b/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/components/ui/dialog/index.ts b/components/ui/dialog/index.ts new file mode 100644 index 0000000..ca8cfea --- /dev/null +++ b/components/ui/dialog/index.ts @@ -0,0 +1,9 @@ +export { default as Dialog } from './Dialog.vue' +export { default as DialogClose } from './DialogClose.vue' +export { default as DialogContent } from './DialogContent.vue' +export { default as DialogDescription } from './DialogDescription.vue' +export { default as DialogFooter } from './DialogFooter.vue' +export { default as DialogHeader } from './DialogHeader.vue' +export { default as DialogScrollContent } from './DialogScrollContent.vue' +export { default as DialogTitle } from './DialogTitle.vue' +export { default as DialogTrigger } from './DialogTrigger.vue' diff --git a/components/ui/hover-card/HoverCard.vue b/components/ui/hover-card/HoverCard.vue new file mode 100644 index 0000000..ea87bf2 --- /dev/null +++ b/components/ui/hover-card/HoverCard.vue @@ -0,0 +1,14 @@ + + + diff --git a/components/ui/hover-card/HoverCardContent.vue b/components/ui/hover-card/HoverCardContent.vue new file mode 100644 index 0000000..687b339 --- /dev/null +++ b/components/ui/hover-card/HoverCardContent.vue @@ -0,0 +1,41 @@ + + + diff --git a/components/ui/hover-card/HoverCardTrigger.vue b/components/ui/hover-card/HoverCardTrigger.vue new file mode 100644 index 0000000..bed301a --- /dev/null +++ b/components/ui/hover-card/HoverCardTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/components/ui/hover-card/index.ts b/components/ui/hover-card/index.ts new file mode 100644 index 0000000..9e4ccc2 --- /dev/null +++ b/components/ui/hover-card/index.ts @@ -0,0 +1,3 @@ +export { default as HoverCard } from './HoverCard.vue' +export { default as HoverCardContent } from './HoverCardContent.vue' +export { default as HoverCardTrigger } from './HoverCardTrigger.vue' diff --git a/package.json b/package.json index ccda95f..868327b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@nuxtjs/tailwindcss": "6.14.0", "@tailwindcss/vite": "^4.1.5", "@uploadthing/nuxt": "^7.1.7", + "@vueuse/core": "^13.1.0", "animate.css": "^4.1.1", "bootstrap-icons": "^1.12.1", "class-variance-authority": "^0.7.1", diff --git a/scraping/.python-version b/scraping/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/scraping/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/scraping/main.py b/scraping/main.py new file mode 100644 index 0000000..d6f9e00 --- /dev/null +++ b/scraping/main.py @@ -0,0 +1,12 @@ +import scrapy + +class BlogSpider(scrapy.Spider): + name = 'blogspider' + start_urls = ['https://news.google.com/u/4/home?hl=zh-TW&gl=TW&ceid=TW:zh-Hant&pageId=none'] + + def parse(self, response): + for title in response.css('.oxy-post-title'): + yield {'title': title.css('::text').get()} + + for next_page in response.css('a.next'): + yield response.follow(next_page, self.parse) \ No newline at end of file diff --git a/scraping/pyproject.toml b/scraping/pyproject.toml new file mode 100644 index 0000000..c9fd0ea --- /dev/null +++ b/scraping/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "scraping" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] diff --git a/scraping/requirements.txt b/scraping/requirements.txt new file mode 100644 index 0000000..ccee6de --- /dev/null +++ b/scraping/requirements.txt @@ -0,0 +1 @@ +scrapy \ No newline at end of file diff --git a/scraping/uv.lock b/scraping/uv.lock new file mode 100644 index 0000000..c3e1b65 --- /dev/null +++ b/scraping/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "scraping" +version = "0.1.0" +source = { virtual = "." } diff --git a/server/api/ai/chat/[slug].ts b/server/api/ai/chat/[slug].ts index e69de29..90f3ea0 100644 --- a/server/api/ai/chat/[slug].ts +++ b/server/api/ai/chat/[slug].ts @@ -0,0 +1,35 @@ +import { Groq } from 'groq-sdk'; +import sql from "~/server/components/postgres"; + +const groq = new Groq(); + +export default defineEventHandler(async (event) => { + const slug = getRouterParam(event, 'slug'); + const body = await readBody(event); + const fetchNewsArticle = await sql` + select * from newArticle + where slug = ${slug} + `; + const chatCompletion = await groq.chat.completions.create({ + "messages": [ + { + "role": "user", + "content": `${body}` + }, + { + "role": "system", + "content": `You are a news chat, the following content will be used to chat with the user title: ${fetchNewsArticle.title}\n content: ${fetchNewsArticle.content}` + } + ], + "model": "llama3-70b-8192", + "temperature": 1, + "max_completion_tokens": 1024, + "top_p": 1, + "stream": true, + "stop": null + }); + + for await (const chunk of chatCompletion) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } +}) \ No newline at end of file diff --git a/server/api/ai/summerize/[slug].ts b/server/api/ai/summerize/[slug].ts index 8a48e48..d3148d0 100644 --- a/server/api/ai/summerize/[slug].ts +++ b/server/api/ai/summerize/[slug].ts @@ -1,15 +1,19 @@ import { Groq } from 'groq-sdk'; +import sql from "~/server/components/postgres"; const groq = new Groq(); export default defineEventHandler(async (event) => { const slug = getRouterParam(event, 'slug'); - const fetchNewsArticle = await fetch(`/api/`); + const fetchNewsArticle = await sql` + select * from newArticle + where slug = ${slug} + `; const chatCompletion = await groq.chat.completions.create({ "messages": [ { "role": "user", - "content": `` + "content": `${fetchNewsArticle.title}\n${fetchNewsArticle.content}` }, { "role": "system", diff --git a/server/api/fetcharticle/[slug].ts b/server/api/fetcharticle/[slug].ts new file mode 100644 index 0000000..6195044 --- /dev/null +++ b/server/api/fetcharticle/[slug].ts @@ -0,0 +1,28 @@ +import sql from "~/server/components/postgres"; +export default defineEventHandler(async (event) => { + const slug = getRouterParam(event, 'slug'); + + // Validate and sanitize the slug + if (!slug || typeof slug !== 'string') { + throw createError({ + statusCode: 400, + message: 'Invalid slug parameter' + }); + } + const cleanSlug = slug.replace(/[^a-zA-Z0-9-_]/g, ''); + + try { + const result = await sql` + select * from articles + where slug = ${cleanSlug} + ` + + return result.rows[0] || null; + } catch (error) { + console.error('Database error:', error); + throw createError({ + statusCode: 500, + message: 'Internal server error' + }); + } +}); \ No newline at end of file