From 37451d28f73234178a50745bd0803273d94a56a5 Mon Sep 17 00:00:00 2001 From: beabigegg Date: Sun, 17 Aug 2025 16:02:54 +0800 Subject: [PATCH] fix2 --- .gitignore | 44 ++++ README.md | 106 +++++++++ USER_MANUAL.md | 60 +++++ __pycache__/ai_routes.cpython-312.pyc | Bin 2905 -> 2190 bytes __pycache__/api_routes.cpython-312.pyc | Bin 23456 -> 24277 bytes __pycache__/tasks.cpython-312.pyc | Bin 22250 -> 22411 bytes ai_routes.py | 11 +- api_routes.py | 14 +- frontend/src/App.css | 43 +--- frontend/src/pages/MeetingDetailPage.jsx | 97 ++++---- frontend/src/services/api.js | 4 +- static/css/style.css | 38 ---- static/js/script.js | 275 ----------------------- tasks.py | 14 +- templates/index.html | 168 -------------- 15 files changed, 291 insertions(+), 583 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 USER_MANUAL.md delete mode 100644 static/css/style.css delete mode 100644 static/js/script.js delete mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab244ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +pip-wheel-metadata/ +.installed.cfg +*.egg-info/ +pip-selfcheck.json + +# Node.js +node_modules/ +dist/ +.npm/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json + +# Environment variables +.env +.env.* +!.env.example + +# IDE / Editor specific +.vscode/ +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS specific +.DS_Store +Thumbs.db + +# Uploads +# Comment out the line below if you want to track uploaded files in git +# Keep it uncommented to prevent tracking user-uploaded content +uploads/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4583764 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# AI Meeting Assistant + +An intelligent meeting assistant designed to streamline your workflow by transcribing, summarizing, and managing action items from your meetings. This full-stack application leverages a Python Flask backend for robust API services and a React frontend for a dynamic user experience. + +## Key Features + +- **User Authentication**: Secure login and registration system with role-based access control (Admin, User). +- **Meeting Management**: Create, view, and manage meetings. Upload transcripts or generate them from audio. +- **AI-Powered Summary**: Automatically generate concise summaries from lengthy meeting transcripts. +- **Action Item Tracking**: Automatically preview and batch-create action items from summaries. Manually add, edit, and delete action items with assigned owners and due dates. +- **Permission Control**: Granular permissions for editing and deleting meetings and action items based on user roles (Admin, Meeting Owner, Action Item Owner). +- **File Processing Tools**: Independent tools for audio extraction, transcription, and text translation. + +## Tech Stack + +**Backend:** +- **Framework**: Flask +- **Database**: SQLAlchemy with Flask-Migrate for schema migrations. +- **Authentication**: Flask-JWT-Extended for token-based security. +- **Async Tasks**: Celery with Redis/RabbitMQ for handling long-running AI tasks. +- **API**: RESTful API design. + +**Frontend:** +- **Framework**: React.js +- **UI Library**: Material-UI (MUI) +- **Tooling**: Vite +- **API Communication**: Axios + +## Prerequisites + +- Python 3.10+ +- Node.js 20.x+ +- A message broker for Celery (e.g., Redis or RabbitMQ) + +## Installation & Setup + +### 1. Backend Setup + +Clone the repository: +```bash +git clone +cd AI_meeting_assistant_-_V2.1 +``` + +Create a virtual environment and install dependencies: +```bash +# For Windows +python -m venv venv +venv\Scripts\activate + +# For macOS/Linux +python3 -m venv venv +source venv/bin/activate + +pip install -r requirements.txt +``` + +Create a `.env` file by copying `.env.example` (if provided) or creating a new one. Configure the following: +``` +FLASK_APP=app.py +SECRET_KEY=your_super_secret_key +SQLALCHEMY_DATABASE_URI=sqlite:///meetings.db # Or your preferred database connection string +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 +``` + +Initialize and apply database migrations: +```bash +flask db init # Only if you don't have a 'migrations' folder +flask db migrate -m "Initial migration" +flask db upgrade +``` + +### 2. Frontend Setup + +Navigate to the frontend directory and install dependencies: +```bash +cd frontend +npm install +``` + +## Running the Application + +The application requires three separate processes to be running: the Flask server, the Celery worker, and the Vite frontend server. + +**1. Start the Flask Backend Server:** +```bash +# From the project root directory +flask run +``` +The API server will be running on `http://127.0.0.1:5000`. + +**2. Start the Celery Worker:** +Open a new terminal, activate the virtual environment, and run: +```bash +# From the project root directory +celery -A celery_worker.celery worker --loglevel=info +``` + +**3. Start the React Frontend Server:** +Open a third terminal and run: +```bash +# From the 'frontend' directory +npm run dev +``` +The frontend application will be available at `http://localhost:5173`. Open this URL in your browser. diff --git a/USER_MANUAL.md b/USER_MANUAL.md new file mode 100644 index 0000000..9a9c51d --- /dev/null +++ b/USER_MANUAL.md @@ -0,0 +1,60 @@ +# AI Meeting Assistant - User Manual + +Welcome to the AI Meeting Assistant! This guide will walk you through the main features of the application and how to use them effectively. + +## 1. Getting Started: Login and Registration + +- **Registration**: If you are a new user, click on the "Register" link on the login page. You will need to provide a unique username and a password to create your account. +- **Login**: Once you have an account, enter your username and password on the login page to access the application. + +## 2. The Dashboard + +After logging in, you will land on the **Dashboard**. This is your main hub for all meetings. + +- **Meeting List**: The dashboard displays a table of all meetings in the system. You can see the meeting's **Topic**, **Owner**, **Meeting Date**, **Status**, and the number of **Action Items**. +- **Sorting**: Click on the column headers (e.g., "Topic", "Meeting Date") to sort the list. +- **Filtering and Searching**: Use the search boxes at the top to find meetings by topic or owner, or filter the list by status. +- **Create a New Meeting**: Click the "New Meeting" button to open a dialog where you can enter a topic and date for a new meeting. Upon creation, you will be taken directly to the Meeting Detail page. +- **View Details**: Click the "View Details" button on any meeting row to navigate to its detail page. +- **Delete a Meeting**: If you are the meeting's creator or an administrator, you will see a delete icon (trash can) to permanently remove the meeting and all its associated data. + +## 3. Meeting Detail Page + +This page is where you'll do most of your work. It's divided into three main sections: Transcript, AI Tools, and Action Items. + +### 3.1. Transcript + +- **View**: This section shows the full transcript of the meeting. +- **Edit**: If you are the meeting owner or an admin, you can click "Edit Transcript" to add, paste, or modify the text content. Click "Save Transcript" to save your changes. + +### 3.2. AI Tools + +This section allows you to leverage AI to process your transcript. + +- **Generate Summary**: + 1. Ensure a transcript has been added. + 2. Click the **"Generate Summary"** button. + 3. A "Generating..." message will appear. The process may take some time depending on the length of the text. + 4. Once complete, a concise summary will appear in the "Summary" box. +- **Edit Summary**: You can also manually edit the generated summary by clicking the "Edit Summary" button. +- **Preview Action Items**: + 1. After a summary or transcript is available, click the **"Preview Action Items"** button. + 2. The AI will analyze the text and display a list of suggested action items in a table. + 3. Review the items. If they are accurate, click **"Save All to List"** to add them to the official "Action Items" list below. + +### 3.3. Action Items + +This is the final list of tasks and to-dos from the meeting. + +- **Add Manually**: Click the "Add Manually" button to open a form where you can create a new action item, assign an owner, and set a due date. +- **Edit an Item**: If you are an Admin, the Meeting Owner, or the assigned owner of an action item, an edit icon (pencil) will appear. Click it to modify the item's details in-line. Click the save icon to confirm. +- **Delete an Item**: If you are an Admin or the Meeting Owner, a delete icon (trash can) will appear, allowing you to remove the action item. +- **Attachments**: You can upload a file attachment when creating or editing an action item. A download icon will appear if an attachment exists. + +## 4. Processing Tools Page + +Accessible from the main navigation, this page provides standalone utilities for file processing. + +1. **Extract Audio**: Upload a video file (e.g., MP4) to extract its audio track into a WAV file, which you can then download. +2. **Transcribe Audio**: Upload an audio file (e.g., WAV, MP3) to generate a text transcript. You can copy the text or download it as a `.txt` file. +3. **Translate Text**: Paste text or upload a `.txt` file, select a target language, and the tool will provide a translation. diff --git a/__pycache__/ai_routes.cpython-312.pyc b/__pycache__/ai_routes.cpython-312.pyc index acc52952fd04f34d7b9c161201e595c5c2776a45..d7ed3c84e6a04a3ff0cd31df213e6cc88a2e505f 100644 GIT binary patch delta 84 zcmca9)+fk!nwOW00SF2^7iP?4naC%>bd6!6#x<5oZcU!eE{uU}%zm1}li9eEC(Ch) kPtN0#pFD|6oJ$|5gAs^}%O-E;D&hLb!p^8!qzn`Q0Fj#$)&Kwi delta 362 zcmeAZyeY5!7S3@OJ zcv=`>Y#1FST*<4+x0#7CkWIWwR6jAfBr`uxH?t%)w^+ZRD77pzwR|%ht2-kbD_CrD zD4SZ8Xnbl#NfA(KJVbdsSW1(nNFHcWkpPI001=u%qL>9pC;&kb$eFh!pk|`zj881c zEYb!_%TGSZmZi+2xIkqC`xPFG2G={>LKEC)gkBI{47tKt9OZ#q%Z$aFiH*U}0y}{2;`@#NIB|D0P8H`7)!*O-9~k YxsOcjjH;iR8JGk>gz872I#saA0H5_$bN~PV diff --git a/__pycache__/api_routes.cpython-312.pyc b/__pycache__/api_routes.cpython-312.pyc index 4906f3e8109c34437d39726a21c05bf5e3da6eab..ea9fd4ce65f7ce25f57db7fb1f1d494e413cc2fa 100644 GIT binary patch delta 4148 zcmZ`+du&tJ8TYY$9TV4a-U)e7CxIj;m^fjCBqW3sXn8awB@ZAQI2Yeb;^Ieo?hTNL z&U8cVHa6PyOte$iE>x8$N;kTBQ>&^}<&SAdR8<(-7`KzQZr!9+(*|nQO|AC$zVF61 zG1Sui`F!8`?m6H4oICdh`^_bmch&9AwZQMQ)8{5W?wrjlw>COv8=q~AR*qIOi`Ak! z6sKCHEPE|?-iC4(%V@RIp}0=5(G_a-qdnsC@}k+>RgyPFh*hjgs!3D(SwN~yQ|ExH zOH+RXYGs;gu?D17X=)m%`ZRUf+A1}qDVMEPYD`nZwt(bIQ!fIwI!*lrsHQaKw+Ey( zDaziYxW`3-tz697izR=W?N^FS_6NXTn_{zO@s7P()JersbBbqgNv)s)JCI_tbz(p& z5kCfAONwV-pA!8iuv=4XwgE+#x1X}io4!r|kFJ|$zE0tETwxAz&2g|~`6O45hSg(1 zIi!c$3b8y zxwEQ}LE;>!4%#Zd)XYlpTW<@i63&{{g%RA68P52zn974;g-6BKnvM|W5u{O;U0Z?6 zv0L4dOZiA{1~42`laSylFF;P(Jj0==5f#{EA$fQR1%|~RYwq{$gW=LLaGBz!Q5sB7 zpvWQ758>Wd+idrJxmmdEqXJxEgu@8v#YdaJ7 zNH}hW0~cCq6ZHzn46cAlho)&S=wYfB;#h4yr`a`74fd?YC1QiUD!t! zr#Qq~{9|n`3y6Z|66q;;?ZsqM^EH+~ihJt_80!!dAC?y%iY$Ca9>o9FFj3VD7Etwhiww2PBJSW~KN;pd#7QQOY3l+dKUb`J z)jYaOT)0}%)SWIavhPv}}UxpPwgGZYdV?B-HZ1*s06@TykeDEdQDoewhjE4M2IA4sA zrH_rzA(swa+8gr;yvqZkqo-PW1*rdtul5`QFTB$e!HQ_=U9q1|=DW7bbn?uhT!O3L z8C&YjFCh0kLYBQ^zPHlxDoi+;c(3{ubL>}W_(v#Y#A4@&Dyb0hn=!V+c=qBe&snm0shjHEAhN8FtM1c7#T<0#Ym zE+VOcakvWDyeQ%kOgBN4GNYG+u#F?Ew_Oy6&XG076TU!QzB||^+;#;`b4DYyD z^Iw2iMKXWar+Ax@`ueR2ysFI<45&OB)-gC48?alJBOn!CM=pi0FxC3$eio-| z)ft>^M!*Eu8~`aF2>(Ex^cFCB#qhvi!QQS%{=?WM(K*6s>2fTj@=M6D zCNNp1qzmM^ntoPgsTt6dmSqUBvM^yekOdl4VL}5Cakn@^T=l$ruucCZ^J76n&u3Hv?C3ejEX>lyt-5cmts$mrg$F)*Ra! ze+2?y`G+j4CmkwI_Q-R&oB(3x2-vB5BtPsvF0P}X1DCfxK*6| LeE-YLj$8d7lB3SK delta 3499 zcmZ`*eQZn7C|=^uc}(9e=uoNrB0JJZQAs@N|P#8+O%rg*8VfC>drYY zc@AFdKYl*v+;i_e-}n9DJ>~M-O2Y^B^#K?DvQJ%^|8V47L!0~0;#}YP9>wK$=|0V` zN3=CJ0@ppH<8sZkYr|Ubd&*3Q-hN_-ytTXK+~@7$?iEI=O1p?U+)*ViIvwsRa$OGh zDRSKoH{_0s9*27px%CeBOZT9-$Ke`1gQC~rraf`d=Wth$QyuQF$Zc@Agf}iWu5ijm zIjcnEi{6mvcVwGnKy=8DP_}7B=80+b@|f}s*(*X~z>y}dEAoiw6){J0@3P*Ts26vn zL#X!&>LnHxbwnQUy{`z_Q8!z8sqTVD#4(&Lm7(A*ciRw>mVY5_7^h31scy>)rMwP9 zvZ3LcZv+>Se6OK(-F74m%0x68g&1uT`BFAlg!_^1mj7zFsU#{_8-*uGC&Dm6SiaTV z=Zg@zMSk4eRlf(xYi?kT5!u{wtbZL!Ce&u2lVJxzlE6#QNU)ROn*{gCS6e2V!$_(E z#z|t|B)@ElgxDfK=-HfM>HvpvxxX?Hddyu{u?nrvV zU+ZsJ?&wTl@s~O`G@ryxs$%&|r;9qIavJ32_0FvsDhRBH8V7fy6lhkgDvBmzsWL2I zc|k+F;7*b_ep)_F&+8~=S2@&Eq;Npq-*tD-AtZ0F=b)`_dk46jtvu7EdB?uq8#*mV zaDsqJ{!D29awkP|MilhN$~n+Ah{(a-ex)E&y_pm{cxSDH)x1@6S(CO}H|Wkff=39J z`(ij2`GnFz^)1{l{PJ(VW!Qp0TVJdFcoH3!`$1Jub`Xr zC2hIK<)wjbOJ7lpU}uQVzTGvR16VAeVX@3D^7WQi(~S&JC#rsXB9LiT(`r9bh6%QI zu&AF-EiVDaMLrViio|F`<9xcP<@J?C1Ac(gOO^An-zvdrB*7raR*H#>-Z0Y~f+tA2 zsybx%miyLIUt0ka(nT~aw_sXzM!8T(gKaWoY0nY)^p?a@0ZBCsT*fRY|U9>pC+g$c#_~d1e`9XHK-+2msJP)am?vF&jBc* zo+_-sO7yTLzfW7o<=n8U#N>0s5%B^F-l^Oe{gSL(W=RDx6(%$RDJfm<)rfiJ?mJ( z?+PYbF7An9r$M@dbOML^cBUrw4#>|z9zy+NriC-)0 z_E5^oAMJh6?wrV~>$&Etosp!ob`x*`bC1wLtGbGV#jQnm!gT_^u%>Tr(86tf^nnaJW8I_X{w=QuUsJyJkv}vX@^94+qU9>zfT58>{y2t@wx7k^C(diYtB%MaF{BvnT zFQyAR{2VWHT-;@U8DMhXATcMTd1WH-pvM7)jy_MT0|Zpch7ZAUuXn0jyp4h(IXU$S zdbj1k7mD((+!-UmUPP&?KotL0@2l7FlO1uPj;qREk2KN^);P}z`^O-+a`3n;?TBlDd0D@Bl^cn$8 zUZB|sG>hOe!BqlkETC!viaAgu*dI3RnhO%Hc9ift=N~PUv~phG3BN}z3Wf2y{A{K# c;8Wb6yAb}_RJrHTKPpO7C3f`3ikBSvA4Fp+CIA2c diff --git a/__pycache__/tasks.cpython-312.pyc b/__pycache__/tasks.cpython-312.pyc index fe3da5c072371635534223a1ef4d40fc77005e9a..6dfca8226e242f2065619238eb56b3a9a01877f6 100644 GIT binary patch delta 805 zcmYL_OK1~O6o&7;lgVQ;lSe0QUQP3uG;Nwl(JBpM46)6lHolMwZdyyGXahpY%yOb# zxG1Lmablk9C1 zN$tkp2Ypt9?AH{+E)OgkvSl)y9jRB)H~ZSg{b7dH9cZ6Ur^Zn)0wTLI&3m^`fx z;0oYZ`M7!4>su%^5yypo;ve~;>6RY2LM!q2x=rVqZJ~Fu&}0E;-(mv|Hn7646gOhy zn`~jbr*AFZ;8Xj_cE5wxDbVTOa`ZPG{jZ}N{OLV_xX`XYxWqPTLPsqY*xY-7CbP|r z_2CB>ocG6IyY{Ggp2&y$DH3j&JcrUP^5{n}2dF3MXeNk0vO~mv0%?B+pwE6M32MNb z>7i?0q_PjH+(W4xG-Z=?Z6K9R(Df)JTp!SpM)d@(a*`ta1<*N@p-8PeF1|F-!(?wu t>mRNyP27+e^97JvXwTVmdzd$ zk5bfw`W>Vw8gD`rDe>T;ph(h-A`KK8N)DbyB-DbU2c0E^E;I8!^UnA3&FsB5_}gpj z_#}!vqTkTb7fWAL>ke*;i4({KW+6QVjNyWj#DK_aT#+%W5XxI%MUh#%)}{$?H{pdb zt7Or{oW_;SqGZZ&)@CucSM5KkU=If|s|VWhR^ zteO*kOt=GwTXmUWiw@LKU?bTh^|qW|q>tzM9Lj{u&QX^UZk^=#B5raf(((?}7=S@l zab89m%JaARn_N@j%J3M-F%dqf^Dvl7MV`<|n9-k7*%el<>DfhXVcA%`k=373OMh-I zhs+uo9;U+ZKDEZ`LE0CtPbc{Zg=IRkOMoXkh-`Df=jj-!%K_-0>9^OszSX$~!-+~A z(lf&gb-BCH|F&&gK2hsA@?frEL85C%2v&t)K`NxTqO;q=T+JVP*Hx9&Um?VMt2WQ? z{UGXe?{xH4JNjOQx1^Chghizm=zb(L2&KRK9CEt%P#f(%9}hw4l$?&z=#I@p(|@w( z>D?b=wUeyHxg=peDwBbP!j!R0H)X|}L~MD2F*$}QR}i6GAvTp`E5eXU*ve^^hBpaI zolTCI+{vo`()!t#R(6!W{Z8rXxm98@e}z5@-!J(1GZg7S*;^&)qKex+*zuDfCfZ;q KHBQ3vCFMULW1PtV diff --git a/ai_routes.py b/ai_routes.py index 392575c..792f060 100644 --- a/ai_routes.py +++ b/ai_routes.py @@ -28,13 +28,4 @@ def summarize_text_api(): summary = _summarize_text(text, user_id=user_id) return jsonify({"summary": summary}) -@ai_bp.post("/action-items/preview") -@jwt_required() -def preview_action_items_api(): - data = request.get_json(force=True) or {} - text = (data.get("text") or "").strip() - if not text: - return jsonify({"error": "text is required"}), 400 - user_id = str(get_jwt_identity() or "user") - items = _extract_action_items(text, user_id=user_id) - return jsonify({"items": items}) + diff --git a/api_routes.py b/api_routes.py index 63bfc5b..4c5aacf 100644 --- a/api_routes.py +++ b/api_routes.py @@ -11,7 +11,8 @@ from tasks import ( extract_audio_task, transcribe_audio_task, translate_text_task, - summarize_text_task + summarize_text_task, + preview_action_items_task ) api_bp = Blueprint("api_bp", __name__, url_prefix="/api") @@ -230,6 +231,17 @@ def summarize_meeting(meeting_id): task = summarize_text_task.delay(meeting_id) return jsonify({'task_id': task.id, 'status_url': f'/status/{task.id}'}), 202 +@api_bp.route('/meetings//preview-actions', methods=['POST']) +@jwt_required() +def preview_actions(meeting_id): + meeting = Meeting.query.get_or_404(meeting_id) + text_content = meeting.summary or meeting.transcript + if not text_content: + return jsonify({'error': 'Meeting has no summary or transcript to analyze.'}), 400 + + task = preview_action_items_task.delay(text_content) + return jsonify({'task_id': task.id, 'status_url': f'/status/{task.id}'}), 202 + # --- Independent Tool Routes --- @api_bp.route('/tools/extract_audio', methods=['POST']) @jwt_required() diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..41d5e8d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,42 +1 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +/* App-specific styles can be added here in the future. */ \ No newline at end of file diff --git a/frontend/src/pages/MeetingDetailPage.jsx b/frontend/src/pages/MeetingDetailPage.jsx index afbd2f7..8f088ff 100644 --- a/frontend/src/pages/MeetingDetailPage.jsx +++ b/frontend/src/pages/MeetingDetailPage.jsx @@ -30,6 +30,7 @@ const MeetingDetailPage = () => { const [isEditingSummary, setIsEditingSummary] = useState(false); const [editData, setEditData] = useState({}); const [summaryTask, setSummaryTask] = useState(null); + const [previewTask, setPreviewTask] = useState(null); // State for the preview task const [actionItems, setActionItems] = useState([]); const [users, setUsers] = useState([]); @@ -39,7 +40,6 @@ const MeetingDetailPage = () => { const [isAddActionItemOpen, setIsAddActionItemOpen] = useState(false); const [newActionItem, setNewActionItem] = useState({ action: '', owner_id: '', due_date: '', item: '' }); const [previewedItems, setPreviewedItems] = useState([]); - const [isPreviewLoading, setIsPreviewLoading] = useState(false); const fetchMeetingData = useCallback(async () => { try { @@ -70,33 +70,44 @@ const MeetingDetailPage = () => { fetchUsers(); }, []); + // Combined polling effect for both summary and preview tasks useEffect(() => { - let intervalId = null; - if (summaryTask && (summaryTask.state === 'PENDING' || summaryTask.state === 'PROGRESS')) { - intervalId = setInterval(async () => { - try { - const updatedTask = await pollTaskStatus(summaryTask.status_url); - if (['SUCCESS', 'FAILURE', 'REVOKED'].includes(updatedTask.state)) { - clearInterval(intervalId); + const task = summaryTask || previewTask; + if (!task || !['PENDING', 'PROGRESS'].includes(task.state)) return; + + const intervalId = setInterval(async () => { + try { + const updatedTask = await pollTaskStatus(task.status_url); + + if (task === summaryTask) setSummaryTask(prev => ({...prev, ...updatedTask})); + if (task === previewTask) setPreviewTask(prev => ({...prev, ...updatedTask})); + + if (['SUCCESS', 'FAILURE', 'REVOKED'].includes(updatedTask.state)) { + clearInterval(intervalId); + if (summaryTask) { // Handle summary success setSummaryTask(null); if (updatedTask.state === 'SUCCESS' && updatedTask.info.summary) { - // Directly update the summary instead of refetching everything - setMeeting(prevMeeting => ({...prevMeeting, summary: updatedTask.info.summary})); - setEditData(prevEditData => ({...prevEditData, summary: updatedTask.info.summary})); - } else { - // Fallback to refetch if something goes wrong or task fails - fetchMeetingData(); + setMeeting(prev => ({...prev, summary: updatedTask.info.summary})); + setEditData(prev => ({...prev, summary: updatedTask.info.summary})); + } + } else if (previewTask) { // Handle preview success + setPreviewTask(null); + if (updatedTask.state === 'SUCCESS' && updatedTask.info.items) { + setPreviewedItems(updatedTask.info.items); } } - } catch (err) { - console.error('Polling failed:', err); - clearInterval(intervalId); - setSummaryTask(null); } - }, 2000); - } + } catch (err) { + console.error('Polling failed:', err); + clearInterval(intervalId); + if (summaryTask) setSummaryTask(null); + if (previewTask) setPreviewTask(null); + } + }, 2000); + return () => clearInterval(intervalId); - }, [summaryTask, fetchMeetingData]); + }, [summaryTask, previewTask, fetchMeetingData]); + const handleSave = async (field, value) => { try { @@ -118,29 +129,25 @@ const MeetingDetailPage = () => { }; const handleGenerateSummary = async () => { - // FIX 3: Set loading state immediately to give instant feedback setSummaryTask({ state: 'PENDING', info: 'Initializing summary task...' }); try { const taskInfo = await summarizeMeeting(meetingId); setSummaryTask({ ...taskInfo, state: 'PENDING' }); } catch (err) { setError('Failed to start summary generation.'); - // Clear the temporary loading state on error setSummaryTask(null); } }; const handlePreviewActionItems = async () => { - const textToPreview = meeting?.summary || meeting?.transcript; - if (!textToPreview) return; - setIsPreviewLoading(true); + setPreviewTask({ state: 'PENDING', info: 'Initializing preview task...' }); try { - const result = await previewActionItems(textToPreview); - setPreviewedItems(result.items || []); + // This now calls the async task endpoint + const taskInfo = await previewActionItems(meetingId); + setPreviewTask({ ...taskInfo, state: 'PENDING' }); } catch (err) { - setError('Failed to generate action item preview.'); - } finally { - setIsPreviewLoading(false); + setError('Failed to start action item preview.'); + setPreviewTask(null); } }; @@ -149,7 +156,7 @@ const MeetingDetailPage = () => { try { await batchSaveActionItems(meetingId, previewedItems); setPreviewedItems([]); - fetchMeetingData(); + fetchMeetingData(); // Refetch to update the main action items list } catch (err) { setError('Failed to save action items.'); } @@ -235,20 +242,18 @@ const MeetingDetailPage = () => { )} - {canManageMeeting && ( - - - {previewedItems.length > 0 && ( - - - Context/ItemActionOwnerDue Date - {previewedItems.map((item, index) => ({item.item}{item.action}{item.owner}{item.due_date}))} -
- -
- )} -
- )} + {canManageMeeting && ( + + {previewedItems.length > 0 && ( + + Context/ItemActionOwnerDue Date + {previewedItems.map((item, index) => ({item.item}{item.action}{item.owner}{item.due_date}))} +
+ +
)} +
)} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index cfee167..6b4fea4 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -76,8 +76,8 @@ export const translateText = (text, target_language) => unwrap(api.post('/tools/translate_text', { text, target_language })); // --- AI Previews (for Meeting Page) --- -export const previewActionItems = (text) => - unwrap(api.post('/action-items/preview', { text })); +export const previewActionItems = (meetingId) => + unwrap(api.post(`/meetings/${meetingId}/preview-actions`)); // --- Action Items --- export const getActionItemsForMeeting = (meetingId) => unwrap(api.get(`/meetings/${meetingId}/action_items`)); diff --git a/static/css/style.css b/static/css/style.css deleted file mode 100644 index 9971d18..0000000 --- a/static/css/style.css +++ /dev/null @@ -1,38 +0,0 @@ -/* static/css/style.css */ -body { - background-color: #f8f9fa; -} - -.container { - max-width: 960px; -} - -.card-header-tabs { - margin-bottom: -1px; -} - -.nav-link { - color: #6c757d; -} - -.nav-link.active { - color: #000; - background-color: #fff; - border-color: #dee2e6 #dee2e6 #fff; -} - -.result-preview { - white-space: pre-wrap; - word-wrap: break-word; - max-height: 400px; - overflow-y: auto; - font-family: 'Courier New', Courier, monospace; -} - -.action-btn:disabled { - cursor: not-allowed; -} - -.progress-bar { - transition: width 0.6s ease; -} diff --git a/static/js/script.js b/static/js/script.js deleted file mode 100644 index b00f9b0..0000000 --- a/static/js/script.js +++ /dev/null @@ -1,275 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // --- Global variables --- - let statusInterval; - let currentTaskType = ''; - let summaryConversationId = null; - let lastSummaryText = ''; - - // --- DOM Elements --- - const progressContainer = document.getElementById('progress-container'); - const statusText = document.getElementById('status-text'); - const progressBar = document.getElementById('progress-bar'); - const resultContainer = document.getElementById('result-container'); - const textResultPreview = document.getElementById('text-result-preview'); - const downloadLink = document.getElementById('download-link'); - const revisionArea = document.getElementById('revision-area'); - const allActionButtons = document.querySelectorAll('.action-btn'); - - // --- Tab Switching Logic --- - const tabButtons = document.querySelectorAll('#myTab button'); - tabButtons.forEach(button => { - button.addEventListener('shown.bs.tab', function() { - resetUiForNewTask(); - }); - }); - - // --- Event Listeners for all action buttons --- - allActionButtons.forEach(button => { - button.addEventListener('click', handleActionClick); - }); - - function handleActionClick(event) { - const button = event.currentTarget; - currentTaskType = button.dataset.task; - - resetUiForNewTask(); - button.disabled = true; - button.innerHTML = ' 處理中...'; - progressContainer.style.display = 'block'; - - if (currentTaskType === 'summarize_text') { - const fileInput = document.getElementById('summary-file-input'); - const file = fileInput.files[0]; - - if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - const fileContent = e.target.result; - startSummarizeTask(fileContent); - }; - reader.onerror = function() { - handleError("讀取檔案時發生錯誤。"); - }; - reader.readAsText(file); - } else { - const textContent = document.getElementById('summary-source-text').value; - if (!textContent.trim()) { - alert('請貼上文字或選擇檔案!'); - resetButtons(); - return; - } - startSummarizeTask(textContent); - } - return; - } - - let endpoint = ''; - let formData = new FormData(); - let body = null; - let fileInput; - - switch (currentTaskType) { - case 'extract_audio': - endpoint = '/extract_audio'; - fileInput = document.getElementById('video-file'); - break; - - case 'transcribe_audio': - endpoint = '/transcribe_audio'; - fileInput = document.getElementById('audio-file'); - formData.append('language', document.getElementById('lang-select').value); - if (document.getElementById('use-demucs').checked) { - formData.append('use_demucs', 'on'); - } - break; - - case 'translate_text': - endpoint = '/translate_text'; - fileInput = document.getElementById('transcript-file'); - formData.append('target_language', document.getElementById('translate-lang-select').value); - break; - - case 'revise_summary': - endpoint = '/summarize_text'; - const instruction = document.getElementById('revision-instruction').value; - if (!lastSummaryText) { alert('請先生成初版結論!'); resetButtons(); return; } - if (!instruction.trim()) { alert('請輸入修改指示!'); resetButtons(); return; } - body = JSON.stringify({ - text_content: lastSummaryText, - revision_instruction: instruction, - target_language: document.getElementById('summary-lang-select').value, - conversation_id: summaryConversationId - }); - startFetchTask(endpoint, body, { 'Content-Type': 'application/json' }); - return; - - default: - console.error('Unknown task type:', currentTaskType); - resetButtons(); - return; - } - - if (!fileInput || !fileInput.files[0]) { - alert('請選擇一個檔案!'); - resetButtons(); - return; - } - formData.append('file', fileInput.files[0]); - body = formData; - - startFetchTask(endpoint, body); - } - - function startSummarizeTask(textContent) { - summaryConversationId = null; - lastSummaryText = textContent; - const body = JSON.stringify({ - text_content: textContent, - target_language: document.getElementById('summary-lang-select').value - }); - startFetchTask('/summarize_text', body, { 'Content-Type': 'application/json' }); - } - - function startFetchTask(endpoint, body, headers = {}) { - updateProgress(0, '準備上傳與處理...'); - fetch(endpoint, { - method: 'POST', - body: body, - headers: headers - }) - .then(response => { - if (!response.ok) { - return response.json().then(err => { throw new Error(err.error || '伺服器錯誤') }); - } - return response.json(); - }) - .then(data => { - if (data.task_id) { - statusInterval = setInterval(() => checkTaskStatus(data.status_url), 2000); - } else { - handleError(data.error || '未能啟動背景任務'); - } - }) - .catch(error => { - handleError(error.message || '請求失敗'); - }); - } - - function checkTaskStatus(statusUrl) { - fetch(statusUrl) - .then(response => response.json()) - .then(data => { - const info = data.info || {}; - if (data.state === 'PROGRESS') { - updateProgress(info.current, info.status, info.total); - const previewContent = info.content || info.summary || info.preview; - if (previewContent) { - resultContainer.style.display = 'block'; - textResultPreview.textContent = previewContent; - textResultPreview.style.display = 'block'; - } - } else if (data.state === 'SUCCESS') { - clearInterval(statusInterval); - updateProgress(100, info.status || '完成!', 100); - displayResult(info); - resetButtons(); - } else if (data.state === 'FAILURE') { - clearInterval(statusInterval); - handleError(info.exc_message || '任務執行失敗'); - } - }) - .catch(error => { - clearInterval(statusInterval); - handleError('查詢進度時發生網路錯誤: ' + error); - }); - } - - function updateProgress(current, text, total = 100) { - const percent = total > 0 ? Math.round((current / total) * 100) : 0; - progressBar.style.width = percent + '%'; - progressBar.setAttribute('aria-valuenow', percent); - progressBar.textContent = percent + '%'; - statusText.textContent = text; - } - - function displayResult(info) { - resultContainer.style.display = 'block'; - - const content = info.content || info.summary; - if (content) { - textResultPreview.textContent = content; - textResultPreview.style.display = 'block'; - lastSummaryText = content; - } else { - textResultPreview.style.display = 'none'; - } - - if (info.download_url) { - downloadLink.href = info.download_url; - downloadLink.style.display = 'inline-block'; - } - - if (currentTaskType === 'summarize_text' || currentTaskType === 'revise_summary') { - revisionArea.style.display = 'block'; - summaryConversationId = info.conversation_id; - } - } - - function handleError(message) { - statusText.textContent = `錯誤:${message}`; - progressBar.classList.add('bg-danger'); - resetButtons(); - } - - function resetUiForNewTask() { - if (statusInterval) clearInterval(statusInterval); - - progressContainer.style.display = 'none'; - resultContainer.style.display = 'none'; - textResultPreview.style.display = 'none'; - textResultPreview.textContent = ''; - downloadLink.style.display = 'none'; - revisionArea.style.display = 'none'; - - progressBar.style.width = '0%'; - progressBar.setAttribute('aria-valuenow', 0); - progressBar.textContent = '0%'; - progressBar.classList.remove('bg-danger'); - statusText.textContent = ''; - - resetButtons(); - } - - function resetButtons() { - allActionButtons.forEach(button => { - button.disabled = false; - const task = button.dataset.task; - let iconHtml = ''; - let text = ''; - - switch(task) { - case 'extract_audio': - iconHtml = ''; - text = '開始轉換'; - break; - case 'transcribe_audio': - iconHtml = ''; - text = '開始轉錄'; - break; - case 'translate_text': - iconHtml = ''; - text = '開始翻譯'; - break; - case 'summarize_text': - iconHtml = ''; - text = '產生初版結論'; - break; - case 'revise_summary': - iconHtml = ''; - text = '根據指示產生修改版'; - break; - } - button.innerHTML = iconHtml + text; - }); - } -}); diff --git a/tasks.py b/tasks.py index a806ecd..5b7e311 100644 --- a/tasks.py +++ b/tasks.py @@ -366,26 +366,38 @@ def preview_action_items_task(self, text_content): self.update_progress(10, 100, "Requesting Dify for action items...") api_key = app.config.get("DIFY_ACTION_EXTRACTOR_API_KEY") plain_text = re.sub(r'^(\s*\[.*?\])\s*', '', text_content, flags=re.MULTILINE) + + if not plain_text.strip(): + return {'status': 'Success', 'items': []} + response = ask_dify(api_key, plain_text) answer_text = response.get("answer", "") self.update_progress(80, 100, "Parsing response...") + parsed_items = [] try: + # Find the JSON array within the response string match = re.search(r'\[.*\]', answer_text, re.DOTALL) if match: json_str = match.group(0) parsed_items = json.loads(json_str) + + # Ensure it's a list, otherwise reset to empty if not isinstance(parsed_items, list): parsed_items = [] + except (json.JSONDecodeError, TypeError): + # If parsing fails, leave it as an empty list parsed_items = [] + self.update_progress(100, 100, "Action item preview generated.") - return {'status': 'Success', 'parsed_items': parsed_items} + return {'status': 'Success', 'items': parsed_items} except Exception as e: self.update_state( state='FAILURE', meta={'exc_type': type(e).__name__, 'exc_message': str(e)} ) + return {'status': 'Error', 'error': str(e)} @celery.task(bind=True) def process_meeting_flow(self, meeting_id, target_language=None): diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index b37dd5d..0000000 --- a/templates/index.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - AI Meeting Assistant - - - - - -
-
-

AI 會議助手

-

一個強大的工具,用於轉錄、翻譯和總結您的會議內容。

-
- -
-
- -
-
-
- -
-
影片轉音訊 (.wav)
-

從影片檔案中提取音軌,以便進行後續處理。

-
- - -
- -
- - -
-
音訊轉文字 (Whisper)
-

將音訊檔案轉錄成帶有時間戳的逐字稿。

-
- - -
-
-
- - -
-
-
- - -
- -
- - -
-
逐段翻譯 (Dify)
-

將逐字稿檔案進行逐段對照翻譯。

-
- - -
-
- - -
- -
- - -
-
會議結論整理 (Dify)
-

從逐字稿或貼上的文字中生成會議摘要。

-
- - -
-
- - -
-
- - -
- -
-
-
-
- - - - - -
- - - - -