mirror of
https://github.com/TeamWiseFlow/wiseflow.git
synced 2025-01-23 02:20:20 +08:00
recommand for awada
This commit is contained in:
parent
61e3c042b4
commit
13131cd88b
15
README.md
15
README.md
@ -9,8 +9,6 @@
|
||||
🌱看看首席情报官是如何帮您节省时间,过滤无关信息,并整理关注要点的吧!🌱
|
||||
|
||||
- ✅ 通用网页内容解析器,综合使用统计学习(依赖开源项目GNE)和LLM,适配90%以上的新闻页面;
|
||||
|
||||
WiseFlow内置微信公号文章专属解析器,但实时获取公众号文章推送需要搭配wxbot,详见示例 [awada](https://github.com/TeamWiseFlow/awada))
|
||||
- ✅ 异步任务架构;
|
||||
- ✅ 使用LLM进行信息提取和标签分类(最低只需使用9B大小的LLM就可完美执行任务)!
|
||||
|
||||
@ -18,7 +16,18 @@ https://github.com/TeamWiseFlow/wiseflow/assets/96130569/bd4b2091-c02d-4457-9ec6
|
||||
|
||||
<img alt="sample.png" src="asset/sample.png" width="1024"/>
|
||||
|
||||
## 🔥 V0.3.1 更新
|
||||
## 🔥 隆重推荐整合了完整RAG能力的 wiseflow 下游应用项目 [awada](https://github.com/TeamWiseFlow/awada) 1.x
|
||||
|
||||
Awada 是一个基于微信生态的团队内知识助理智能体。它可以从群聊、公众号、网站等来源中进行在线自主学习(同时也接受自主文档上传),打造团队私域知识库,并为团队成员提供问答、资料查找以及写作(Word)服务。
|
||||
|
||||
如果你的关注点并不是信息列表,而是基于信息的下游应用,那么 awada 将是一个不错的选择。
|
||||
|
||||
Awada 整合了 wiseflow 的在线学习能力和 [Qanything](https://github.com/netease-youdao/QAnything) 的 RAG 能力,**如果你更加关注基于微信生态的信息搜集(比如公众号文章),也请参考 awada项目**
|
||||
|
||||
## V0.3.1 更新
|
||||
|
||||
dashboard 部分已经删除,如果您有dashboard需求,请下载 [V0.2.1版本](https://github.com/TeamWiseFlow/wiseflow/releases/tag/V0.2.1)
|
||||
|
||||
|
||||
👏 虽然部分9b大小的LLM(THUDM/glm-4-9b-chat)已经可以实现稳定的信息提取输出,但是我们发现对于复杂含义的tag(比如“党建”)或者需要特指的tag(比如仅需采集“居民区活动”,而不希望包括诸如演唱会这样的大型活动信息),
|
||||
使用目前的prompt还是不能进行准确的提取,因此我们在这一版本中为每个tag增加了explaination字段,可以通过输入该字段进行更加清晰的tag指定。
|
||||
|
14
README_EN.md
14
README_EN.md
@ -9,8 +9,6 @@
|
||||
🌱 See how Chief Intelligence Officer helps you save time, filter out irrelevant information, and organize key points of interest! 🌱
|
||||
|
||||
- ✅ Universal web content parser, comprehensively using statistical learning (dependent on the open-source project GNE) and LLM, suitable for over 90% of news pages;
|
||||
|
||||
WiseFlow has a built-in WeChat official account article exclusive parser, but real-time access to official account article push needs to be matched with wxbot, see the example for details [awada](https://github.com/TeamWiseFlow/awada)
|
||||
- ✅ Asynchronous task architecture;
|
||||
- ✅ Information extraction and label classification using LLM (only requires an LLM of 9B size to perfectly execute tasks)!
|
||||
|
||||
@ -18,7 +16,17 @@ https://github.com/TeamWiseFlow/wiseflow/assets/96130569/bd4b2091-c02d-4457-9ec6
|
||||
|
||||
<img alt="sample.png" src="asset/sample.png" width="1024"/>
|
||||
|
||||
## 🔥 V0.3.1 Update
|
||||
## 🔥 Highly Recommended Downstream Application Project [awada](https://github.com/TeamWiseFlow/awada) 1.x with Full RAG Capabilities Integrated
|
||||
|
||||
Awada is an intelligent agent for team knowledge within the WeChat ecosystem. It can autonomously learn online from sources such as group chats, official accounts, websites, and also accepts manual document uploads, creating a private knowledge base for the team. It provides services such as Q&A, material search, and writing (Word) for team members.
|
||||
|
||||
If your focus is not on the information list but on downstream applications based on information, then awada is a good choice.
|
||||
|
||||
Awada integrates the online learning capabilities of wiseflow and the RAG capabilities of [Qanything](https://github.com/netease-youdao/QAnything). **If you are more concerned with information collection within the WeChat ecosystem (such as official account articles), please also refer to the awada project**.
|
||||
|
||||
## V0.3.1 Update
|
||||
|
||||
The dashboard part has been removed. If you have a dashboard requirement, please download the [V0.2.1 version](https://github.com/TeamWiseFlow/wiseflow/releases/tag/V0.2.1).
|
||||
|
||||
👏 Although some 9B-sized LLMs (THUDM/glm-4-9b-chat) can already achieve stable information extraction output, we found that for complex meaning tags (like "Party Building") or tags that require specific collection (like only collecting "community activities" without including large events like concerts),
|
||||
the current prompts cannot perform accurate extraction. Therefore, in this version, we have added an explanation field for each tag, which allows for clearer tag specification through input.
|
||||
|
14
README_JP.md
14
README_JP.md
@ -9,8 +9,6 @@
|
||||
🌱 最高情報責任者がどのようにあなたの時間を節約し、無関係な情報をフィルタリングし、注目すべきポイントを整理するかを見てみましょう! 🌱
|
||||
|
||||
- ✅ 汎用ウェブコンテンツパーサー、統計学習(オープンソースプロジェクトGNEに依存)とLLMを包括的に使用し、90%以上のニュースページに適合;
|
||||
|
||||
WiseFlowにはWeChat公式アカウント記事専用パーサーが組み込まれていますが、公式アカウント記事のプッシュをリアルタイムで取得するにはwxbotが必要です。詳しくは例を参照してください [awada](https://github.com/TeamWiseFlow/awada)
|
||||
- ✅ 非同期タスクアーキテクチャ;
|
||||
- ✅ LLMを使用した情報抽出とラベル分類(9BサイズのLLMで完璧にタスクを実行できます)!
|
||||
|
||||
@ -18,7 +16,17 @@ https://github.com/TeamWiseFlow/wiseflow/assets/96130569/bd4b2091-c02d-4457-9ec6
|
||||
|
||||
<img alt="sample.png" src="asset/sample.png" width="1024"/>
|
||||
|
||||
## 🔥 V0.3.1 アップデート
|
||||
## 🔥 完全統合されたRAG機能を持つ wiseflow の下流アプリケーションプロジェクト [awada](https://github.com/TeamWiseFlow/awada) 1.x を強くお勧めします
|
||||
|
||||
Awada は、WeChatエコシステム内のチーム知識アシスタントインテリジェントエージェントです。グループチャット、公式アカウント、ウェブサイトなどのソースからオンラインで自律的に学習し(手動も可能)、チームのプライベートナレッジベースを構築し、チームメンバーにQ&A、資料検索、および書き込み(Word)サービスを提供します。
|
||||
|
||||
あなたの焦点が情報リストではなく、情報に基づく下流アプリケーションである場合、awada は良い選択です。
|
||||
|
||||
Awada は、wiseflow のオンライン学習機能と [Qanything](https://github.com/netease-youdao/QAnything) の RAG 機能を統合しています。**WeChatエコシステム内の情報収集(例えば公式アカウントの記事)により関心がある場合は、awada プロジェクトも参照してください**。
|
||||
|
||||
## V0.3.1 アップデート
|
||||
|
||||
ダッシュボード部分は削除されました。ダッシュボードが必要な場合は、[V0.2.1 バージョン](https://github.com/TeamWiseFlow/wiseflow/releases/tag/V0.2.1) をダウンロードしてください。
|
||||
|
||||
👏 9BサイズのLLM(THUDM/glm-4-9b-chat)の一部は、安定した情報抽出出力を実現できますが、複雑な意味のタグ(「党建設」など)や特定の収集が必要なタグ(「コミュニティ活動」のみを収集し、コンサートなどの大規模なイベント情報は含まない)については、
|
||||
現在のプロンプトでは正確な抽出ができません。そこで、このバージョンでは各タグに説明フィールドを追加し、入力によってより明確なタグ指定ができるようにしました。
|
||||
|
14
README_KR.md
14
README_KR.md
@ -9,8 +9,6 @@
|
||||
🌱 수석 정보 책임자가 어떻게 당신의 시간을 절약하고, 관련 없는 정보를 필터링하며, 주목할 만한 요점을 정리하는지 살펴보세요! 🌱
|
||||
|
||||
- ✅ 범용 웹 콘텐츠 파서, 통계 학습(오픈 소스 프로젝트 GNE에 의존)과 LLM을 포괄적으로 사용하여 90% 이상의 뉴스 페이지에 적합;
|
||||
|
||||
WiseFlow에는 WeChat 공식 계정 기사 전용 파서가 내장되어 있지만 공식 계정 기사 푸시에 대한 실시간 액세스는 wxbot과 일치해야 합니다. 자세한 내용은 예를 참조하십시오 [awada](https://github.com/TeamWiseFlow/awada)
|
||||
- ✅ 비동기 작업 아키텍처;
|
||||
- ✅ LLM을 사용한 정보 추출 및 라벨 분류 (9B 크기의 LLM으로 작업을 완벽하게 수행할 수 있습니다)!
|
||||
|
||||
@ -18,7 +16,17 @@ https://github.com/TeamWiseFlow/wiseflow/assets/96130569/bd4b2091-c02d-4457-9ec6
|
||||
|
||||
<img alt="sample.png" src="asset/sample.png" width="1024"/>
|
||||
|
||||
## 🔥 V0.3.1 업데이트
|
||||
## 🔥 완전히 통합된 RAG 기능을 갖춘 wiseflow 다운스트림 애플리케이션 프로젝트 [awada](https://github.com/TeamWiseFlow/awada) 1.x 강력 추천
|
||||
|
||||
Awada는 위챗 생태계 내 팀 지식 보조 인텔리전트 에이전트입니다. 그룹 채팅, 공식 계정, 웹사이트 등의 소스에서 온라인으로 자율적으로 학습하고(수동 문서 업로드도 가능) 팀의 프라이빗 네트워크 지식 베이스를 구축하며, 팀 멤버에게 Q&A, 자료 검색 및 글쓰기(Word) 서비스를 제공합니다.
|
||||
|
||||
귀하의 관심사가 정보 목록이 아니라 정보 기반의 다운스트림 애플리케이션이라면, awada는 좋은 선택입니다.
|
||||
|
||||
Awada는 wiseflow의 온라인 학습 기능과 [Qanything](https://github.com/netease-youdao/QAnything)의 RAG 기능을 통합하고 있습니다. **위챗 생태계 내 정보 수집(예: 공식 계정 기사)에 더 관심이 있다면, awada 프로젝트도 참조하십시오**.
|
||||
|
||||
## V0.3.1 업데이트
|
||||
|
||||
대시보드 부분이 삭제되었습니다. 대시보드가 필요하다면, [V0.2.1 버전](https://github.com/TeamWiseFlow/wiseflow/releases/tag/V0.2.1)을 다운로드하십시오.
|
||||
|
||||
👏 일부 9B 크기의 LLM(THUDM/glm-4-9b-chat)은 이미 안정적인 정보 추출 출력을 달성할 수 있지만, 복잡한 의미의 태그(예: "당 건설") 또는 특정 수집이 필요한 태그(예: "커뮤니티 활동"만 수집하고 콘서트와 같은 대규모 이벤트 정보는 포함하지 않음)에 대해서는
|
||||
현재 프롬프트로는 정확한 추출을 수행할 수 없습니다. 따라서 이 버전에서는 각 태그에 설명 필드를 추가하여 입력을 통해 더 명확한 태그 지정이 가능하도록 했습니다.
|
||||
|
@ -1,2 +0,0 @@
|
||||
VITE_API_BASE=http://localhost:7777
|
||||
VITE_PB_BASE=http://localhost:8090
|
@ -1,2 +0,0 @@
|
||||
VITE_API_BASE=http://localhost:7777
|
||||
VITE_PB_BASE=http://localhost:8090
|
@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
}
|
24
dashboard/web/.gitignore
vendored
24
dashboard/web/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -1,6 +0,0 @@
|
||||
web env:
|
||||
VITE_API_BASE=http://localhost:7777
|
||||
VITE_PB_BASE=http://localhost:8090
|
||||
|
||||
pocketase env:
|
||||
AW_FILE_DIR=xxx
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>情报分析</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "asweb-react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.9.6",
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
"@tanstack/react-query-devtools": "^5.17.9",
|
||||
"axios": "^1.6.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"pocketbase": "^0.21.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"redaxios": "^0.5.1",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"wouter": "^3.1.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"postcss": "^8.4.33",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"rollup": "npm:@rollup/wasm-node"
|
||||
}
|
||||
}
|
||||
}
|
3374
dashboard/web/pnpm-lock.yaml
generated
3374
dashboard/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,13 +0,0 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
min-height: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider, QueryCache, useQueryClient } from "@tanstack/react-query"
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
|
||||
import "./App.css"
|
||||
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import LoginScreen from "@/components/screen/login"
|
||||
// import Steps from "@/components/screen/steps"
|
||||
import InsightsScreen from "@/components/screen/insights"
|
||||
import ArticlesScreen from "@/components/screen/articles"
|
||||
import ReportScreen from "@/components/screen/report"
|
||||
|
||||
import { isAuth } from "@/store"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
import { Route, Switch, useLocation } from "wouter"
|
||||
|
||||
function App() {
|
||||
const [, setLocation] = useLocation()
|
||||
if (!isAuth()) {
|
||||
setLocation("/login")
|
||||
}
|
||||
// const { toast } = useToast()
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Switch>
|
||||
<Route path='/' component={InsightsScreen} />
|
||||
<Route path='/login' component={LoginScreen} />
|
||||
<Route path='/insights' component={InsightsScreen} />
|
||||
<Route path='/articles' component={ArticlesScreen} />
|
||||
<Route path='/report/:insight_id' component={ReportScreen} />
|
||||
<Route>404</Route>
|
||||
</Switch>
|
||||
{/* <Button
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: "Scheduled: Catch up",
|
||||
description: "Friday, February 10, 2023 at 5:57 PM",
|
||||
})
|
||||
}}
|
||||
>
|
||||
Show Toast
|
||||
</Button> */}
|
||||
<Toaster />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB |
@ -1,33 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Delete } from "lucide-react"
|
||||
|
||||
// data expecting object {"0":{}, "1":{}}
|
||||
export function ArticleList({ data, showActions, onDelete }) {
|
||||
return (
|
||||
<div className='grid w-full gap-1.5'>
|
||||
<div className='border overflow-hidden'>
|
||||
{data &&
|
||||
data.map((article, i) => (
|
||||
<div key={i} className='border-b px-4 py-2 flex gap-2'>
|
||||
<div className='flex-1 whitespace-nowrap min-w-0'>
|
||||
<p className='font-normal w-full truncate underline text-left'>
|
||||
<a href={article.url} target='_blank' rel='noreferrer'>
|
||||
{article.expand?.translation_result?.title || article.title}
|
||||
</a>
|
||||
</p>
|
||||
<p className='font-light min-w-0 truncate text-left'>{article.expand?.translation_result?.abstract || article.abstract}</p>
|
||||
</div>
|
||||
<div>
|
||||
{showActions && (
|
||||
<Button variant='ghost' className='text-red-500' onClick={() => onDelete(article.id)}>
|
||||
<Delete className='h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{data && <p className='text-sm text-muted-foreground mt-4'>共{Object.keys(data).length}篇文章</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export default function StepLayout({ title, description, children, navigate }) {
|
||||
return (
|
||||
<>
|
||||
<div className='mx-auto text-left'>
|
||||
<div className='flex gap-4'>
|
||||
<div className='flex-1'>
|
||||
<h1 className='mt-10 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0'>{title}</h1>
|
||||
{description && <p className='text-xl text-muted-foreground'>{description}</p>}
|
||||
</div>
|
||||
{/* <Button variant='outline' onClick={() => navigate("/start")}>
|
||||
新建任务
|
||||
</Button> */}
|
||||
</div>
|
||||
<hr className='my-4'></hr>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import { useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArticleList } from "../article-list"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { Languages } from "lucide-react"
|
||||
import { ButtonLoading } from "@/components/ui/button-loading"
|
||||
import { useDatePager, useArticleDates, useArticles, translations } from "@/store"
|
||||
|
||||
import { useLocation } from "wouter"
|
||||
|
||||
function ArticlesScreen({}) {
|
||||
const [, navigate] = useLocation()
|
||||
|
||||
const queryDates = useArticleDates()
|
||||
const { index, last, next, hasLast, hasNext } = useDatePager(queryDates.data)
|
||||
const currentDate = queryDates.data && index >= 0 ? queryDates.data[index] : ""
|
||||
const query = useArticles(currentDate)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: (data) => {
|
||||
return translations(data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["articles", currentDate] })
|
||||
},
|
||||
})
|
||||
|
||||
function trans() {
|
||||
mut.mutate({ article_ids: query.data.filter((d) => !d.translation_result).map((d) => d.id) })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>文章</h2>
|
||||
{query.isError && <p className='text-red-500 my-4'>{query.error.message}</p>}
|
||||
<div className='my-6 flex gap-4 w-fit'>
|
||||
<Button onClick={() => navigate("/insights")}>查看分析结果</Button>
|
||||
{mut.isPending && <ButtonLoading />}
|
||||
{!mut.isPending && query.data && query.data.length > 0 && query.data.filter((a) => !a.translation_result).length > 0 && (
|
||||
<Button variant='outline' className='text-blue-400 mb-6' onClick={trans}>
|
||||
<Languages className='w-4 h-4' />
|
||||
一键翻译
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{currentDate && (
|
||||
<div className='my-6 flex gap-4 flex items-center'>
|
||||
<Button disabled={!hasLast()} variant='outline' onClick={last}>
|
||||
<
|
||||
</Button>
|
||||
<p>{currentDate}</p>
|
||||
<Button disabled={!hasNext()} variant='outline' onClick={next}>
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* {completed && !Object.values(query.data.articles)[0]["zh-cn"] && (
|
||||
<Button variant='link' className='text-blue-400 mb-6' onClick={trans}>
|
||||
<Languages className='w-4 h-4' />
|
||||
一键翻译
|
||||
</Button>
|
||||
)} */}
|
||||
|
||||
{query.data && <ArticleList data={query.data} />}
|
||||
|
||||
<div className='my-6 flex gap-4'>
|
||||
<Button onClick={() => navigate("/insights")}>查看分析结果</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArticlesScreen
|
@ -1,160 +0,0 @@
|
||||
import { useEffect } from "react"
|
||||
import { useLocation } from "wouter"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { Files } from "lucide-react"
|
||||
import { ArticleList } from "@/components/article-list"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { ButtonLoading } from "@/components/ui/button-loading"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||
import { useClientStore, useInsights, unlinkArticle, useInsightDates, useDatePager, more } from "@/store"
|
||||
|
||||
function List({ insights, selected, onOpen, onDelete, onReport, onMore, isGettingMore, error }) {
|
||||
function change(value) {
|
||||
if (value) onOpen(value)
|
||||
}
|
||||
|
||||
function unlink(article_id) {
|
||||
onDelete(selected, article_id)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion type='single' collapsible onValueChange={change} className='w-full'>
|
||||
{insights.map((insight, i) => (
|
||||
<AccordionItem value={insight.id} key={i}>
|
||||
<AccordionTrigger className='hover:no-underline'>
|
||||
<div className='px-4 py-2 cursor-pointer flex items-center gap-2 overflow-hidden'>
|
||||
{selected === insight.id && <div className='-ml-4 w-2 h-2 bg-green-400 rounded-full'></div>}
|
||||
<p className={"truncate text-wrap text-left flex-1 " + (selected === insight.id ? "font-bold" : "font-normal")}>{insight.content}</p>
|
||||
<div className='flex items-center justify-center gap-1'>
|
||||
<Files className='h-4 w-4 text-slate-400' />
|
||||
<span className='text-slate-400 text-sm leading-none'>x {insight.expand.articles.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className='px-4'>
|
||||
<ArticleList data={insight.expand.articles} showActions={true} onDelete={unlink} />
|
||||
{error && <p className='text-red-500 my-4'>{error.message}</p>}
|
||||
|
||||
{(isGettingMore && <ButtonLoading />) || (
|
||||
<div className='flex gap-4 justify-center'>
|
||||
<Button onClick={onReport} className='my-4'>
|
||||
生成报告
|
||||
</Button>
|
||||
<Button variant='outline' onClick={onMore} className='my-4'>
|
||||
搜索更多
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
function InsightsScreen({}) {
|
||||
const selectedInsight = useClientStore((state) => state.selectedInsight)
|
||||
const selectInsight = useClientStore((state) => state.selectInsight)
|
||||
const dates = useInsightDates()
|
||||
const { index, last, next, hasLast, hasNext } = useDatePager(dates)
|
||||
// console.log(dates, index)
|
||||
const currentDate = dates.length > 0 && index >= 0 ? dates[index] : ""
|
||||
const data = useInsights(currentDate)
|
||||
// console.log(data)
|
||||
const [, navigate] = useLocation()
|
||||
const queryClient = useQueryClient()
|
||||
const mut = useMutation({
|
||||
mutationFn: (params) => {
|
||||
if (params && selectedInsight && data.find((insight) => insight.id == selectedInsight).expand.articles.length == 1) {
|
||||
throw new Error("不能删除最后一篇文章")
|
||||
}
|
||||
return unlinkArticle(params)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["insights", currentDate] })
|
||||
},
|
||||
})
|
||||
|
||||
const mutMore = useMutation({
|
||||
mutationFn: (data) => {
|
||||
return more(data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["insights", currentDate] })
|
||||
},
|
||||
})
|
||||
|
||||
const { toast } = useToast()
|
||||
const queryCache = queryClient.getQueryCache()
|
||||
queryCache.onError = (error) => {
|
||||
console.log("error in cache", error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "出错啦!",
|
||||
description: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
selectInsight(null)
|
||||
}, [index])
|
||||
|
||||
useEffect(() => {
|
||||
mut.reset() // only show error with the selected insight
|
||||
}, [selectedInsight])
|
||||
|
||||
function unlink(insight_id, article_id) {
|
||||
mut.mutate({ insight_id, article_id })
|
||||
}
|
||||
|
||||
function report() {
|
||||
navigate("/report/" + selectedInsight)
|
||||
}
|
||||
|
||||
function getMore() {
|
||||
console.log()
|
||||
mutMore.mutate({ insight_id: selectedInsight })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>分析结果</h2>
|
||||
{currentDate && (
|
||||
<div className='my-6 flex gap-4 flex items-center'>
|
||||
<Button disabled={!hasLast()} variant='outline' onClick={last}>
|
||||
<
|
||||
</Button>
|
||||
<p>{currentDate}</p>
|
||||
<Button disabled={!hasNext()} variant='outline' onClick={next}>
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className='grid w-full gap-1.5'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<div className='flex-1'>{<p className=''>选择一项结果生成文档</p>}</div>
|
||||
</div>
|
||||
<div className='w-full gap-1.5'>
|
||||
<div className=''>
|
||||
<List insights={data} selected={selectedInsight} onOpen={(id) => selectInsight(id)} onDelete={unlink} onReport={report} onMore={getMore} isGettingMore={mutMore.isPending} error={mut.error} />
|
||||
</div>
|
||||
<p className='text-sm text-muted-foreground mt-4'>共{Object.keys(data).length}条结果</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='my-6 flex flex-col gap-4 w-36 text-left'>
|
||||
<Button variant='outline' onClick={() => navigate("/articles")}>
|
||||
查看所有文章
|
||||
</Button>
|
||||
<a href={`${import.meta.env.VITE_PB_BASE}/_/`} target='__blank' className='text-sm underline'>
|
||||
数据库管理 >
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InsightsScreen
|
@ -1,82 +0,0 @@
|
||||
// import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
// import * as z from 'zod'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import { useLocation } from 'wouter'
|
||||
import { login } from '@/store'
|
||||
|
||||
// const FormSchema = z.object({
|
||||
// username: z.string().nonempty('请填写用户名'),
|
||||
// password: z.string().nonempty('请填写密码'),
|
||||
// })
|
||||
|
||||
export function AdminLoginScreen() {
|
||||
const form = useForm({
|
||||
// resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
})
|
||||
|
||||
const [, setLocation] = useLocation()
|
||||
const mutation = useMutation({
|
||||
mutationFn: login,
|
||||
onSuccess: (data) => {
|
||||
setLocation('/')
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(e) {
|
||||
mutation.mutate({ username: form.getValues('username'), password: form.getValues('password') })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-sm mx-auto text-left">
|
||||
<h2 className="mt-10 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">登录</h2>
|
||||
<p className="text-xl text-muted-foreground">输入账号及密码</p>
|
||||
<hr className="my-6"></hr>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="mx-auto space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem className="text-left">
|
||||
<FormLabel>用户名</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription></FormDescription>
|
||||
<FormMessage>{mutation?.error?.response?.data?.['identity']?.message}</FormMessage>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="text-left">
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormDescription></FormDescription>
|
||||
<FormMessage>{mutation?.error?.response?.data?.['password']?.message}</FormMessage>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<p className="text-sm text-destructive">{mutation?.error?.message}</p>
|
||||
<Button type="submit">登录</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminLoginScreen
|
@ -1,99 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ButtonLoading } from "@/components/ui/button-loading"
|
||||
import { FileDown } from "lucide-react"
|
||||
import { useClientStore, report, useInsight } from "@/store"
|
||||
import { useEffect } from "react"
|
||||
import { useLocation, useParams } from "wouter"
|
||||
|
||||
function ReportScreen({}) {
|
||||
// const selectedInsight = useClientStore((state) => state.selectedInsight)
|
||||
// const workflow_name = useClientStore((state) => state.workflow_name)
|
||||
// const taskId = useClientStore((state) => state.taskId)
|
||||
// const [wasWorking, setWasWorking] = useState(false)
|
||||
|
||||
const toc = useClientStore((state) => state.toc)
|
||||
const updateToc = useClientStore((state) => state.updateToc)
|
||||
const comment = useClientStore((state) => state.comment)
|
||||
const updateComment = useClientStore((state) => state.updateComment)
|
||||
|
||||
const [, navigate] = useLocation()
|
||||
const params = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (!params || !params.insight_id) {
|
||||
console.log("expect /report/[insight_id]")
|
||||
navigate("/insights", { replace: true })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const query = useInsight(params.insight_id)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: async (data) => report(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["insight", params.insight_id] })
|
||||
},
|
||||
})
|
||||
|
||||
function changeToc(e) {
|
||||
let lines = e.target.value.split("\n")
|
||||
if (lines.length == 1 && lines[0] == "") lines = []
|
||||
// updateToc(lines.filter((l) => l.trim()))
|
||||
updateToc(lines)
|
||||
}
|
||||
|
||||
function changeComment(e) {
|
||||
updateComment(e.target.value)
|
||||
}
|
||||
|
||||
function submit(e) {
|
||||
mut.mutate({ toc: toc, insight_id: params.insight_id, comment: comment })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='text-left'>
|
||||
<div>
|
||||
<h2 className='max-w-screen-md'>报告生成</h2>
|
||||
<h3 className='my-4'>已选择分析结果:</h3>
|
||||
{query.data && <div className='bg-slate-100 px-4 py-2 mb-4 text-slate-600 max-w-screen-md'>{query.data.content}</div>}
|
||||
</div>
|
||||
<div className='grid gap-1.5'>
|
||||
<h3 className='my-4'>报告大纲:</h3>
|
||||
<Textarea placeholder='' id='outline' rows='10' value={toc.join("\n")} onChange={changeToc} className='max-w-screen-md ' />
|
||||
<small>首行输入标题,每个纲目或章节单独一行. 首行空白自动生成标题. </small>
|
||||
{query.data?.docx && <Input placeholder='修改意见' className='mt-6 max-w-screen-md ' value={comment} onChange={changeComment} />}
|
||||
</div>
|
||||
<div className='my-6 flex flex-col gap-4 w-max '>
|
||||
{(mut.isPending && <ButtonLoading />) || (
|
||||
<Button disabled={toc.length <= 0} onClick={submit}>
|
||||
{query.data?.docx ? "再次生成" : "生成"}
|
||||
</Button>
|
||||
)}
|
||||
{!mut.isPending && (
|
||||
<Button variant='outline' onClick={() => navigate("/insights")}>
|
||||
选择其他分析结果
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!mut.isPending && query.data?.docx && (
|
||||
<div className='grid gap-1.5 max-w-screen-md border rounded px-4 py-2 pb-6 '>
|
||||
<p className='my-4'>报告已生成,点击下载</p>
|
||||
<p className='bg-slate-100 px-4 py-2 hover:underline flex gap-2 items-center overflow-hidden'>
|
||||
<FileDown className='h-4 w-4 text-slate-400' />
|
||||
<a className='truncate ' href={`${import.meta.env.VITE_PB_BASE}/api/files/${query.data.collectionName}/${query.data.id}/${query.data.docx}`} target='_blank' rel='noreferrer'>
|
||||
{query.data.docx}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{query.isError && <p className='text-red-500 my-4'>{query.error.message}</p>}
|
||||
{mut.isError && <p className='text-red-500 my-4'>{mut.error.message}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportScreen
|
@ -1,58 +0,0 @@
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ButtonLoading } from "@/components/ui/button-loading"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Minus, Plus, Loader2 } from "lucide-react"
|
||||
import { useClientStore, createTask } from "@/store"
|
||||
|
||||
function StartScreen({ navigate, id }) {
|
||||
const s = useClientStore()
|
||||
const mut = useMutation({
|
||||
mutationFn: (data) => createTask(data),
|
||||
onSuccess: () => {
|
||||
//query.invalidate()
|
||||
navigate("/articles")
|
||||
},
|
||||
})
|
||||
|
||||
function change(e) {
|
||||
let urls = e.target.value.split("\n")
|
||||
if (urls.length == 1 && urls[0] == "") urls = []
|
||||
s.setUrls(urls)
|
||||
}
|
||||
|
||||
function submit() {
|
||||
mut.mutate({ urls: s.urls, days: s.days })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='grid w-full gap-1.5'>
|
||||
<Label htmlFor='message'>网站清单</Label>
|
||||
<Textarea placeholder='每行输入一个网站的主域名,以http://或https://开头' id='message' rows='20' value={s.urls.join("\n")} onChange={change} />
|
||||
{s.countUrls() > 0 && <p className='text-sm text-muted-foreground'>共{s.countUrls()}个网站</p>}
|
||||
<div className='my-6 select-none'>
|
||||
仅抓取
|
||||
<Button variant='outline' size='icon' disabled={s.minDays()} className='mx-2' onClick={s.decr}>
|
||||
<Minus className='h-4 w-4' />
|
||||
</Button>
|
||||
<span className='font-mono'>{s.days}</span>
|
||||
<Button variant='outline' size='icon' disabled={s.maxDays()} className='mx-2' onClick={s.incr}>
|
||||
<Plus className='h-4 w-4' />
|
||||
</Button>
|
||||
天内更新的文章
|
||||
</div>
|
||||
</div>
|
||||
{mut.isError && <p className='text-red-500 my-4'>{mut.error.message}</p>}
|
||||
{(mut.isPending && <ButtonLoading />) || (
|
||||
<Button disabled={s.countUrls() == 0} onClick={submit}>
|
||||
{mut.isLoading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
|
||||
提交
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StartScreen
|
@ -1,105 +0,0 @@
|
||||
import { useState, useTransition } from "react"
|
||||
import { Banner } from "@/components/ui/banner"
|
||||
import StepLayout from "@/components/layout/step"
|
||||
import StartScreen from "@/components/screen/start"
|
||||
import ArticlesScreen from "@/components/screen/articles"
|
||||
import InsightsScreen from "@/components/screen/insights"
|
||||
import ReportScreen from "@/components/screen/report"
|
||||
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useEffect } from "react"
|
||||
import { useClientStore, useData } from "@/store"
|
||||
|
||||
const TITLE = "情报分析"
|
||||
|
||||
function Steps() {
|
||||
let [currentScreen, setCurrentScreen] = useState("/insights")
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const selectInsight = useClientStore((state) => state.selectInsight)
|
||||
const selectedInsight = useClientStore((state) => state.selectedInsight)
|
||||
const taskId = useClientStore((state) => state.taskId)
|
||||
const setTaskId = useClientStore((state) => state.setTaskId)
|
||||
|
||||
// useEffect(() => {
|
||||
// const searchParams = new URLSearchParams(document.location.search)
|
||||
// let taskIdSpecified = searchParams.get("task_id")
|
||||
// if (taskIdSpecified) {
|
||||
// setTaskId(taskIdSpecified)
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
// const query = useData(taskId)
|
||||
// console.log(taskId, query.data)
|
||||
|
||||
// useEffect(() => {
|
||||
// // navigate away from /start
|
||||
// if (query.data && currentScreen == "/start") {
|
||||
// let state = query.data
|
||||
// if (state.articles && Object.keys(state.articles).length > 0) {
|
||||
// setCurrentScreen("/articles")
|
||||
// }
|
||||
|
||||
// if (state.insights && Object.keys(state.insights).length > 0) {
|
||||
// if (selectedInsight && state.insights[selectedInsight]?.report?.file) {
|
||||
// setCurrentScreen("/report")
|
||||
// } else {
|
||||
// setCurrentScreen("/insights")
|
||||
// }
|
||||
// } else {
|
||||
// selectInsight(null) // deselect
|
||||
// }
|
||||
// }
|
||||
// }, [query.data])
|
||||
|
||||
// const errors = (query.isError && [query.error]) || (query.data && query.data.errors && query.data.errors.length > 0 && query.data.errors)
|
||||
|
||||
function navigate(screen) {
|
||||
startTransition(() => {
|
||||
setCurrentScreen(screen)
|
||||
})
|
||||
}
|
||||
|
||||
// console.log("screen:", currenScreen)
|
||||
|
||||
let content, title
|
||||
if (currentScreen == "/start") {
|
||||
title = TITLE + " > " + "数据来源"
|
||||
content = <StartScreen navigate={navigate} />
|
||||
} else if (currentScreen == "/articles") {
|
||||
title = TITLE + " > " + "文章列表"
|
||||
content = <ArticlesScreen navigate={navigate} />
|
||||
} else if (currentScreen == "/insights") {
|
||||
title = TITLE + " > " + "分析结果"
|
||||
content = <InsightsScreen navigate={navigate} />
|
||||
} else if (currentScreen == "/report") {
|
||||
title = TITLE + " > " + "生成报告"
|
||||
content = <ReportScreen navigate={navigate} />
|
||||
}
|
||||
|
||||
return (
|
||||
<StepLayout title={title} isPending={isPending} navigate={navigate}>
|
||||
{content}
|
||||
{/* {errors && (
|
||||
<Banner>
|
||||
{errors.map((e, i) => (
|
||||
<p key={i}>{e}</p>
|
||||
))}
|
||||
</Banner>
|
||||
)} */}
|
||||
|
||||
{/* {query.data && query.data.working && (
|
||||
<div className='fixed bottom-2 right-2 text-sm'>
|
||||
<Loader2 className='w-4 h-4 animate-spin text-red-500'></Loader2>
|
||||
</div>
|
||||
)} */}
|
||||
{/* {query.isFetching && (
|
||||
<div className='fixed bottom-2 right-2 text-sm'>
|
||||
<Loader2 className='w-4 h-4 animate-spin'></Loader2>
|
||||
</div>
|
||||
)} */}
|
||||
{/* <div className='left-8 bottom-8 text-sm text-muted-foreground mt-8'>task_id:{taskId}</div> */}
|
||||
</StepLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Steps
|
@ -1,41 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
@ -1,9 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Banner = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className={cn("max-h-24 overflow-y-scroll fixed top-0 right-8 min-w-[200px] max-w-[500px] ml-0 mr-0 bg-red-100 text-red-900 px-6 py-2 border border-red-400 rounded-sm shadow-sm text-sm disabled:cursor-not-allowed", className)} ref={ref} {...props} />
|
||||
))
|
||||
Banner.displayName = "Banner"
|
||||
|
||||
export { Banner }
|
@ -1,11 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export function ButtonLoading() {
|
||||
return (
|
||||
<Button disabled>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
请稍后
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
(<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
@ -1,133 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
const FormFieldContext = React.createContext({})
|
||||
|
||||
const FormField = (
|
||||
{
|
||||
...props
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
(<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>)
|
||||
);
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext({})
|
||||
|
||||
const FormItem = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
(<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>)
|
||||
);
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
(<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
(<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
(<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
(<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}>
|
||||
{body}
|
||||
</p>)
|
||||
);
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
(<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
@ -1,16 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
@ -1,18 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
@ -1,82 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
(<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
|
@ -1,33 +0,0 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
(<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
(<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>)
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>)
|
||||
);
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST"
|
||||
}
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map()
|
||||
|
||||
const addToRemoveQueue = (toastId) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t),
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const listeners = []
|
||||
|
||||
let memoryState = { toasts: [] }
|
||||
|
||||
function dispatch(action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
function toast({
|
||||
...props
|
||||
}) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
};
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
@ -1,82 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
h2 {
|
||||
@apply scroll-m-20 text-2xl font-semibold tracking-tight mb-4 text-left;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export const LOCAL_TIME_OFFSITE = "+08:00"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
// var d = new Date(isNaN(date) ? date + "T00:00:00" + LOCAL_TIME_OFFSITE : date) // local
|
||||
var d = new Date(isNaN(date) ? date + "T00:00:00" : date) // utc
|
||||
var iso = d.toISOString()
|
||||
return iso.slice(0, 10) + " " + iso.slice(11, 23) + "Z"
|
||||
// return [d.getFullYear(), (d.getMonth() + 1).padLeft(), d.getDate().padLeft()].join("-") + " " + [d.getHours().padLeft(), d.getMinutes().padLeft(), d.getSeconds().padLeft()].join(":") + ".000Z"
|
||||
}
|
||||
|
||||
Number.prototype.padLeft = function (base, chr) {
|
||||
var len = String(base || 10).length - String(this).length + 1
|
||||
return len > 0 ? new Array(len).join(chr || "0") + this : this
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
@ -1,276 +0,0 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import PocketBase from "pocketbase"
|
||||
const pb = new PocketBase(import.meta.env.VITE_PB_BASE)
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { create } from "zustand"
|
||||
import { persist } from "zustand/middleware"
|
||||
// import axios from "redaxios"
|
||||
import axios from "axios"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
import { formatDate, LOCAL_TIME_OFFSITE } from "./lib/utils"
|
||||
|
||||
const DAYS_RANGE = [1, 14]
|
||||
|
||||
export const useClientStore = create(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
taskId: "",
|
||||
urls: ["https://cyberscoop.com"],
|
||||
days: 14,
|
||||
workflow_name: "情报分析",
|
||||
toc: ["参考情报", "基本内容", "相关发声情况", "应对策略"],
|
||||
selectedInsight: null,
|
||||
comment: "",
|
||||
|
||||
setTaskId: (taskId) => set({ taskId }),
|
||||
setUrls: (urls) => set({ urls }),
|
||||
countUrls: () => get().urls.filter((url) => url).length,
|
||||
selectInsight: (id) => set({ selectedInsight: id }),
|
||||
updateToc: (value) => set({ toc: value }),
|
||||
updateComment: (value) => set({ comment: value }),
|
||||
incr: () => set((state) => ({ days: state.days + 1 > DAYS_RANGE[1] ? DAYS_RANGE[1] : state.days + 1 })),
|
||||
decr: () => set((state) => ({ days: state.days - 1 < DAYS_RANGE[0] ? DAYS_RANGE[0] : state.days - 1 })),
|
||||
minDays: () => get().days === DAYS_RANGE[0],
|
||||
maxDays: () => get().days === DAYS_RANGE[1],
|
||||
}),
|
||||
{
|
||||
version: "0.1.1",
|
||||
name: "aw-storage",
|
||||
// storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export function login({ username, password }) {
|
||||
//return pb.collection("users").authWithPassword(username, password)
|
||||
return pb.admins.authWithPassword(username, password)
|
||||
}
|
||||
|
||||
export function isAuth() {
|
||||
return pb.authStore.isValid
|
||||
}
|
||||
|
||||
export function useData(task_id, autoRefetch = undefined) {
|
||||
let interval = parseInt(autoRefetch) >= 1000 ? parseInt(autoRefetch) : undefined
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["data", task_id ? task_id : ""],
|
||||
queryFn: () => data(task_id ? task_id : ""),
|
||||
refetchInterval: (query) => {
|
||||
//console.log(query)
|
||||
if (!query.state.data || (query.state.data && query.state.data.working)) {
|
||||
return interval
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createTask({ id, urls, days }) {
|
||||
let from = new Date()
|
||||
from.setHours(0, 0, 0, 0)
|
||||
from.setDate(from.getDate() - days)
|
||||
|
||||
let fromStr = from.toISOString().slice(0, 10).split("-").join("")
|
||||
let task_id = id || nanoid(10)
|
||||
console.log("creating task: ", task_id, urls.filter((url) => url).length + " sites", fromStr)
|
||||
|
||||
if (urls.length == 0) {
|
||||
urls.push("")
|
||||
}
|
||||
|
||||
return axios({
|
||||
method: "post",
|
||||
url: `${import.meta.env.VITE_API_BASE}/sites`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
after: fromStr,
|
||||
sites: urls,
|
||||
task_id: task_id,
|
||||
},
|
||||
})
|
||||
.then(function (response) {
|
||||
useClientStore.getState().setTaskId(task_id)
|
||||
return response
|
||||
})
|
||||
.catch(function (error) {
|
||||
useClientStore.getState().setTaskId("")
|
||||
return error
|
||||
})
|
||||
}
|
||||
|
||||
export function report({ task_id, insight_id, toc, comment }) {
|
||||
return axios({
|
||||
method: "post",
|
||||
url: `${import.meta.env.VITE_API_BASE}/report`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
task_id: task_id,
|
||||
toc: toc,
|
||||
insight_id: insight_id,
|
||||
comment: comment,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function more({ insight_id }) {
|
||||
return axios({
|
||||
method: "post",
|
||||
url: `${import.meta.env.VITE_API_BASE}/search_for_insight`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
//toc: toc,
|
||||
insight_id: insight_id,
|
||||
//comment: comment,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function translations({ article_ids }) {
|
||||
return axios({
|
||||
method: "post",
|
||||
url: `${import.meta.env.VITE_API_BASE}/translations`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
article_ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useArticles(date) {
|
||||
return useQuery({
|
||||
queryKey: ["articles", date],
|
||||
queryFn: () => getArticles(date),
|
||||
})
|
||||
}
|
||||
|
||||
export function useInsight(id) {
|
||||
return useQuery({
|
||||
queryKey: ["insight", id],
|
||||
queryFn: () => getInsight(id),
|
||||
})
|
||||
}
|
||||
|
||||
export function useInsights(date) {
|
||||
const { data = [] } = useQuery({
|
||||
queryKey: ["insights", date],
|
||||
queryFn: () => getInsights(date),
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export function useInsightDates() {
|
||||
const { data = [] } = useQuery({
|
||||
queryKey: ["insight_dates"],
|
||||
queryFn: getInsightDates,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export function useArticleDates() {
|
||||
return useQuery({
|
||||
queryKey: ["article_dates"],
|
||||
queryFn: () => getArticleDates(),
|
||||
})
|
||||
}
|
||||
|
||||
export function useDatePager(dates) {
|
||||
const [index, setIndex] = useState(-1)
|
||||
|
||||
useEffect(() => {
|
||||
if (index < 0 && dates) {
|
||||
setIndex(dates.length - 1)
|
||||
}
|
||||
}, [index, dates])
|
||||
|
||||
const hasLast = () => index > 0
|
||||
const hasNext = () => index >= 0 && index < dates.length - 1
|
||||
const last = () => hasLast() && setIndex(index - 1)
|
||||
const next = () => hasNext() && setIndex(index + 1)
|
||||
|
||||
return {
|
||||
index,
|
||||
last,
|
||||
next,
|
||||
hasLast,
|
||||
hasNext,
|
||||
}
|
||||
}
|
||||
|
||||
export function getArticles(date) {
|
||||
if (!date) return []
|
||||
|
||||
const from = formatDate(date)
|
||||
//const to = formatDate(new Date(new Date(date + "T00:00:00" + LOCAL_TIME_OFFSITE).getTime() + 60 * 60 * 24 * 1000))
|
||||
const to = formatDate(new Date(new Date(date + "T00:00:00").getTime() + 60 * 60 * 24 * 1000))
|
||||
console.log("from/to", from, to)
|
||||
return pb.collection("articles").getFullList({
|
||||
sort: "-created",
|
||||
expand: "translation_result",
|
||||
filter: 'created >= "' + from + '" && created < "' + to + '"',
|
||||
})
|
||||
}
|
||||
|
||||
export function getInsight(id) {
|
||||
return pb.collection("insights").getOne(id, { expand: "docx" })
|
||||
}
|
||||
|
||||
export function getInsights(date) {
|
||||
if (!date) return []
|
||||
|
||||
const from = formatDate(date)
|
||||
//const to = formatDate(new Date(new Date(date + "T00:00:00" + LOCAL_TIME_OFFSITE).getTime() + 60 * 60 * 24 * 1000))
|
||||
const to = formatDate(new Date(new Date(date + "T00:00:00").getTime() + 60 * 60 * 24 * 1000))
|
||||
// console.log("from/to", from, to)
|
||||
|
||||
const f = 'created >= "' + from + '" && created < "' + to + '"'
|
||||
// console.log(f)
|
||||
return pb.collection("insights").getFullList({
|
||||
sort: "-created",
|
||||
expand: "articles, articles.translation_result",
|
||||
// expand: "articles",
|
||||
filter: f,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getInsightDates() {
|
||||
const { data } = await axios({
|
||||
method: "get",
|
||||
url: `${import.meta.env.VITE_PB_BASE}/insight_dates`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + pb.authStore?.token,
|
||||
},
|
||||
})
|
||||
//return data.map((d) => new Date(d + "T00:00:00" + LOCAL_TIME_OFFSITE).toISOString().slice(0, 10))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getArticleDates() {
|
||||
let { data } = await axios({
|
||||
method: "get",
|
||||
url: `${import.meta.env.VITE_PB_BASE}/article_dates`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + pb.authStore?.token,
|
||||
},
|
||||
})
|
||||
//return data.map((d) => new Date(d + "T00:00:00" + LOCAL_TIME_OFFSITE).toISOString().slice(0, 10))
|
||||
return data
|
||||
}
|
||||
|
||||
export function unlinkArticle({ insight_id, article_id }) {
|
||||
return pb.collection("insights").update(insight_id, {
|
||||
"articles-": article_id,
|
||||
})
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{js,jsx}',
|
||||
'./components/**/*.{js,jsx}',
|
||||
'./app/**/*.{js,jsx}',
|
||||
'./src/**/*.{js,jsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import path from "path"
|
||||
import * as child from "child_process"
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
// const commitHash = child.execSync('git rev-parse --short HEAD').toString()
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
// define: { 'import.meta.env.VITE_APP_VERSION': JSON.stringify(commitHash) },
|
||||
})
|
Loading…
Reference in New Issue
Block a user