mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 01:36:30 +02:00
chore: ran linting
This commit is contained in:
parent
772150eb66
commit
de8841fb86
110 changed files with 2673 additions and 1918 deletions
|
|
@ -298,8 +298,7 @@ async def create_surfsense_deep_agent(
|
|||
|
||||
# Disable Google Drive action tools if no Google Drive connector is configured
|
||||
has_google_drive_connector = (
|
||||
available_connectors is not None
|
||||
and "GOOGLE_DRIVE_FILE" in available_connectors
|
||||
available_connectors is not None and "GOOGLE_DRIVE_FILE" in available_connectors
|
||||
)
|
||||
if not has_google_drive_connector:
|
||||
google_drive_tools = [
|
||||
|
|
@ -337,8 +336,7 @@ async def create_surfsense_deep_agent(
|
|||
|
||||
# Disable Jira action tools if no Jira connector is configured
|
||||
has_jira_connector = (
|
||||
available_connectors is not None
|
||||
and "JIRA_CONNECTOR" in available_connectors
|
||||
available_connectors is not None and "JIRA_CONNECTOR" in available_connectors
|
||||
)
|
||||
if not has_jira_connector:
|
||||
jira_tools = [
|
||||
|
|
|
|||
|
|
@ -43,11 +43,16 @@ def create_create_confluence_page_tool(
|
|||
logger.info(f"create_confluence_page called: title='{title}'")
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Confluence tool not properly configured."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Confluence tool not properly configured.",
|
||||
}
|
||||
|
||||
try:
|
||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
||||
context = await metadata_service.get_creation_context(search_space_id, user_id)
|
||||
context = await metadata_service.get_creation_context(
|
||||
search_space_id, user_id
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
return {"status": "error", "message": context["error"]}
|
||||
|
|
@ -60,22 +65,28 @@ def create_create_confluence_page_tool(
|
|||
"connector_type": "confluence",
|
||||
}
|
||||
|
||||
approval = interrupt({
|
||||
"type": "confluence_page_creation",
|
||||
"action": {
|
||||
"tool": "create_confluence_page",
|
||||
"params": {
|
||||
"title": title,
|
||||
"content": content,
|
||||
"space_id": space_id,
|
||||
"connector_id": connector_id,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "confluence_page_creation",
|
||||
"action": {
|
||||
"tool": "create_confluence_page",
|
||||
"params": {
|
||||
"title": title,
|
||||
"content": content,
|
||||
"space_id": space_id,
|
||||
"connector_id": connector_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -84,7 +95,10 @@ def create_create_confluence_page_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The page was not created."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The page was not created.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -114,12 +128,16 @@ def create_create_confluence_page_tool(
|
|||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "No Confluence connector found."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No Confluence connector found.",
|
||||
}
|
||||
actual_connector_id = connector.id
|
||||
else:
|
||||
result = await db_session.execute(
|
||||
|
|
@ -127,15 +145,21 @@ def create_create_confluence_page_tool(
|
|||
SearchSourceConnector.id == actual_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Confluence connector is invalid.",
|
||||
}
|
||||
|
||||
try:
|
||||
client = ConfluenceHistoryConnector(session=db_session, connector_id=actual_connector_id)
|
||||
client = ConfluenceHistoryConnector(
|
||||
session=db_session, connector_id=actual_connector_id
|
||||
)
|
||||
api_result = await client.create_page(
|
||||
space_id=final_space_id,
|
||||
title=final_title,
|
||||
|
|
@ -143,7 +167,10 @@ def create_create_confluence_page_tool(
|
|||
)
|
||||
await client.close()
|
||||
except Exception as api_err:
|
||||
if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower():
|
||||
if (
|
||||
"http 403" in str(api_err).lower()
|
||||
or "status code 403" in str(api_err).lower()
|
||||
):
|
||||
try:
|
||||
_conn = connector
|
||||
_conn.config = {**_conn.config, "auth_expired": True}
|
||||
|
|
@ -163,6 +190,7 @@ def create_create_confluence_page_tool(
|
|||
kb_message_suffix = ""
|
||||
try:
|
||||
from app.services.confluence import ConfluenceKBSyncService
|
||||
|
||||
kb_service = ConfluenceKBSyncService(db_session)
|
||||
kb_result = await kb_service.sync_after_create(
|
||||
page_id=page_id,
|
||||
|
|
@ -189,9 +217,13 @@ def create_create_confluence_page_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error creating Confluence page: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while creating the page."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while creating the page.",
|
||||
}
|
||||
|
||||
return create_confluence_page
|
||||
|
|
|
|||
|
|
@ -39,14 +39,21 @@ def create_delete_confluence_page_tool(
|
|||
- If status is "not_found", relay the message to the user.
|
||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
||||
"""
|
||||
logger.info(f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'")
|
||||
logger.info(
|
||||
f"delete_confluence_page called: page_title_or_id='{page_title_or_id}'"
|
||||
)
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Confluence tool not properly configured."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Confluence tool not properly configured.",
|
||||
}
|
||||
|
||||
try:
|
||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
||||
context = await metadata_service.get_deletion_context(search_space_id, user_id, page_title_or_id)
|
||||
context = await metadata_service.get_deletion_context(
|
||||
search_space_id, user_id, page_title_or_id
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
error_msg = context["error"]
|
||||
|
|
@ -67,21 +74,27 @@ def create_delete_confluence_page_tool(
|
|||
document_id = page_data["document_id"]
|
||||
connector_id_from_context = context.get("account", {}).get("id")
|
||||
|
||||
approval = interrupt({
|
||||
"type": "confluence_page_deletion",
|
||||
"action": {
|
||||
"tool": "delete_confluence_page",
|
||||
"params": {
|
||||
"page_id": page_id,
|
||||
"connector_id": connector_id_from_context,
|
||||
"delete_from_kb": delete_from_kb,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "confluence_page_deletion",
|
||||
"action": {
|
||||
"tool": "delete_confluence_page",
|
||||
"params": {
|
||||
"page_id": page_id,
|
||||
"connector_id": connector_id_from_context,
|
||||
"delete_from_kb": delete_from_kb,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -90,7 +103,10 @@ def create_delete_confluence_page_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The page was not deleted."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The page was not deleted.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -102,33 +118,47 @@ def create_delete_confluence_page_tool(
|
|||
final_params = decision["args"]
|
||||
|
||||
final_page_id = final_params.get("page_id", page_id)
|
||||
final_connector_id = final_params.get("connector_id", connector_id_from_context)
|
||||
final_connector_id = final_params.get(
|
||||
"connector_id", connector_id_from_context
|
||||
)
|
||||
final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb)
|
||||
|
||||
from sqlalchemy.future import select
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||
|
||||
if not final_connector_id:
|
||||
return {"status": "error", "message": "No connector found for this page."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No connector found for this page.",
|
||||
}
|
||||
|
||||
result = await db_session.execute(
|
||||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.id == final_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Confluence connector is invalid.",
|
||||
}
|
||||
|
||||
try:
|
||||
client = ConfluenceHistoryConnector(session=db_session, connector_id=final_connector_id)
|
||||
client = ConfluenceHistoryConnector(
|
||||
session=db_session, connector_id=final_connector_id
|
||||
)
|
||||
await client.delete_page(final_page_id)
|
||||
await client.close()
|
||||
except Exception as api_err:
|
||||
if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower():
|
||||
if (
|
||||
"http 403" in str(api_err).lower()
|
||||
or "status code 403" in str(api_err).lower()
|
||||
):
|
||||
try:
|
||||
connector.config = {**connector.config, "auth_expired": True}
|
||||
flag_modified(connector, "config")
|
||||
|
|
@ -146,6 +176,7 @@ def create_delete_confluence_page_tool(
|
|||
if final_delete_from_kb and document_id:
|
||||
try:
|
||||
from app.db import Document
|
||||
|
||||
doc_result = await db_session.execute(
|
||||
select(Document).filter(Document.id == document_id)
|
||||
)
|
||||
|
|
@ -171,9 +202,13 @@ def create_delete_confluence_page_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error deleting Confluence page: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while deleting the page."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while deleting the page.",
|
||||
}
|
||||
|
||||
return delete_confluence_page
|
||||
|
|
|
|||
|
|
@ -41,14 +41,21 @@ def create_update_confluence_page_tool(
|
|||
- If status is "not_found", relay the message to the user.
|
||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
||||
"""
|
||||
logger.info(f"update_confluence_page called: page_title_or_id='{page_title_or_id}'")
|
||||
logger.info(
|
||||
f"update_confluence_page called: page_title_or_id='{page_title_or_id}'"
|
||||
)
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Confluence tool not properly configured."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Confluence tool not properly configured.",
|
||||
}
|
||||
|
||||
try:
|
||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
||||
context = await metadata_service.get_update_context(search_space_id, user_id, page_title_or_id)
|
||||
context = await metadata_service.get_update_context(
|
||||
search_space_id, user_id, page_title_or_id
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
error_msg = context["error"]
|
||||
|
|
@ -71,24 +78,30 @@ def create_update_confluence_page_tool(
|
|||
document_id = page_data.get("document_id")
|
||||
connector_id_from_context = context.get("account", {}).get("id")
|
||||
|
||||
approval = interrupt({
|
||||
"type": "confluence_page_update",
|
||||
"action": {
|
||||
"tool": "update_confluence_page",
|
||||
"params": {
|
||||
"page_id": page_id,
|
||||
"document_id": document_id,
|
||||
"new_title": new_title,
|
||||
"new_content": new_content,
|
||||
"version": current_version,
|
||||
"connector_id": connector_id_from_context,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "confluence_page_update",
|
||||
"action": {
|
||||
"tool": "update_confluence_page",
|
||||
"params": {
|
||||
"page_id": page_id,
|
||||
"document_id": document_id,
|
||||
"new_title": new_title,
|
||||
"new_content": new_content,
|
||||
"version": current_version,
|
||||
"connector_id": connector_id_from_context,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -97,7 +110,10 @@ def create_update_confluence_page_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The page was not updated."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The page was not updated.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -114,29 +130,40 @@ def create_update_confluence_page_tool(
|
|||
if final_content is None:
|
||||
final_content = current_body
|
||||
final_version = final_params.get("version", current_version)
|
||||
final_connector_id = final_params.get("connector_id", connector_id_from_context)
|
||||
final_connector_id = final_params.get(
|
||||
"connector_id", connector_id_from_context
|
||||
)
|
||||
final_document_id = final_params.get("document_id", document_id)
|
||||
|
||||
from sqlalchemy.future import select
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||
|
||||
if not final_connector_id:
|
||||
return {"status": "error", "message": "No connector found for this page."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No connector found for this page.",
|
||||
}
|
||||
|
||||
result = await db_session.execute(
|
||||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.id == final_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Confluence connector is invalid.",
|
||||
}
|
||||
|
||||
try:
|
||||
client = ConfluenceHistoryConnector(session=db_session, connector_id=final_connector_id)
|
||||
client = ConfluenceHistoryConnector(
|
||||
session=db_session, connector_id=final_connector_id
|
||||
)
|
||||
await client.update_page(
|
||||
page_id=final_page_id,
|
||||
title=final_title,
|
||||
|
|
@ -145,7 +172,10 @@ def create_update_confluence_page_tool(
|
|||
)
|
||||
await client.close()
|
||||
except Exception as api_err:
|
||||
if "http 403" in str(api_err).lower() or "status code 403" in str(api_err).lower():
|
||||
if (
|
||||
"http 403" in str(api_err).lower()
|
||||
or "status code 403" in str(api_err).lower()
|
||||
):
|
||||
try:
|
||||
connector.config = {**connector.config, "auth_expired": True}
|
||||
flag_modified(connector, "config")
|
||||
|
|
@ -163,6 +193,7 @@ def create_update_confluence_page_tool(
|
|||
if final_document_id:
|
||||
try:
|
||||
from app.services.confluence import ConfluenceKBSyncService
|
||||
|
||||
kb_service = ConfluenceKBSyncService(db_session)
|
||||
kb_result = await kb_service.sync_after_update(
|
||||
document_id=final_document_id,
|
||||
|
|
@ -171,12 +202,18 @@ def create_update_confluence_page_tool(
|
|||
search_space_id=search_space_id,
|
||||
)
|
||||
if kb_result["status"] == "success":
|
||||
kb_message_suffix = " Your knowledge base has also been updated."
|
||||
kb_message_suffix = (
|
||||
" Your knowledge base has also been updated."
|
||||
)
|
||||
else:
|
||||
kb_message_suffix = " The knowledge base will be updated in the next sync."
|
||||
kb_message_suffix = (
|
||||
" The knowledge base will be updated in the next sync."
|
||||
)
|
||||
except Exception as kb_err:
|
||||
logger.warning(f"KB sync after update failed: {kb_err}")
|
||||
kb_message_suffix = " The knowledge base will be updated in the next sync."
|
||||
kb_message_suffix = (
|
||||
" The knowledge base will be updated in the next sync."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
|
@ -186,9 +223,13 @@ def create_update_confluence_page_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error updating Confluence page: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while updating the page."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while updating the page.",
|
||||
}
|
||||
|
||||
return update_confluence_page
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ def create_create_gmail_draft_tool(
|
|||
- "Draft an email to alice@example.com about the meeting"
|
||||
- "Compose a reply to Bob about the project update"
|
||||
"""
|
||||
logger.info(
|
||||
f"create_gmail_draft called: to='{to}', subject='{subject}'"
|
||||
)
|
||||
logger.info(f"create_gmail_draft called: to='{to}', subject='{subject}'")
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {
|
||||
|
|
@ -187,7 +185,10 @@ def create_create_gmail_draft_tool(
|
|||
f"Creating Gmail draft: to='{final_to}', subject='{final_subject}', connector={actual_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -251,10 +252,12 @@ def create_create_gmail_draft_tool(
|
|||
try:
|
||||
created = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: gmail_service.users()
|
||||
.drafts()
|
||||
.create(userId="me", body={"message": {"raw": raw}})
|
||||
.execute(),
|
||||
lambda: (
|
||||
gmail_service.users()
|
||||
.drafts()
|
||||
.create(userId="me", body={"message": {"raw": raw}})
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
@ -289,9 +292,7 @@ def create_create_gmail_draft_tool(
|
|||
}
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"Gmail draft created: id={created.get('id')}"
|
||||
)
|
||||
logger.info(f"Gmail draft created: id={created.get('id')}")
|
||||
|
||||
kb_message_suffix = ""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -56,9 +56,7 @@ def create_send_gmail_email_tool(
|
|||
- "Send an email to alice@example.com about the meeting"
|
||||
- "Email Bob the project update"
|
||||
"""
|
||||
logger.info(
|
||||
f"send_gmail_email called: to='{to}', subject='{subject}'"
|
||||
)
|
||||
logger.info(f"send_gmail_email called: to='{to}', subject='{subject}'")
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {
|
||||
|
|
@ -188,7 +186,10 @@ def create_send_gmail_email_tool(
|
|||
f"Sending Gmail email: to='{final_to}', subject='{final_subject}', connector={actual_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -252,10 +253,12 @@ def create_send_gmail_email_tool(
|
|||
try:
|
||||
sent = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: gmail_service.users()
|
||||
.messages()
|
||||
.send(userId="me", body={"raw": raw})
|
||||
.execute(),
|
||||
lambda: (
|
||||
gmail_service.users()
|
||||
.messages()
|
||||
.send(userId="me", body={"raw": raw})
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
|
|||
|
|
@ -186,7 +186,10 @@ def create_trash_gmail_email_tool(
|
|||
f"Trashing Gmail email: message_id='{final_message_id}', connector={final_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -241,10 +244,12 @@ def create_trash_gmail_email_tool(
|
|||
try:
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: gmail_service.users()
|
||||
.messages()
|
||||
.trash(userId="me", id=final_message_id)
|
||||
.execute(),
|
||||
lambda: (
|
||||
gmail_service.users()
|
||||
.messages()
|
||||
.trash(userId="me", id=final_message_id)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
@ -257,7 +262,10 @@ def create_trash_gmail_email_tool(
|
|||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
if not connector.config.get("auth_expired"):
|
||||
connector.config = {**connector.config, "auth_expired": True}
|
||||
connector.config = {
|
||||
**connector.config,
|
||||
"auth_expired": True,
|
||||
}
|
||||
flag_modified(connector, "config")
|
||||
await db_session.commit()
|
||||
except Exception:
|
||||
|
|
@ -273,9 +281,7 @@ def create_trash_gmail_email_tool(
|
|||
}
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"Gmail email trashed: message_id={final_message_id}"
|
||||
)
|
||||
logger.info(f"Gmail email trashed: message_id={final_message_id}")
|
||||
|
||||
trash_result: dict[str, Any] = {
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -216,7 +216,10 @@ def create_update_gmail_draft_tool(
|
|||
f"Updating Gmail draft: subject='{final_subject}', connector={final_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -299,14 +302,16 @@ def create_update_gmail_draft_tool(
|
|||
try:
|
||||
updated = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: gmail_service.users()
|
||||
.drafts()
|
||||
.update(
|
||||
userId="me",
|
||||
id=final_draft_id,
|
||||
body={"message": {"raw": raw}},
|
||||
)
|
||||
.execute(),
|
||||
lambda: (
|
||||
gmail_service.users()
|
||||
.drafts()
|
||||
.update(
|
||||
userId="me",
|
||||
id=final_draft_id,
|
||||
body={"message": {"raw": raw}},
|
||||
)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
@ -369,7 +374,9 @@ def create_update_gmail_draft_tool(
|
|||
document.document_metadata = meta
|
||||
flag_modified(document, "document_metadata")
|
||||
await db_session.commit()
|
||||
kb_message_suffix = " Your knowledge base has also been updated."
|
||||
kb_message_suffix = (
|
||||
" Your knowledge base has also been updated."
|
||||
)
|
||||
logger.info(
|
||||
f"KB document {document_id} updated for draft {final_draft_id}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ def create_create_calendar_event_tool(
|
|||
|
||||
accounts = context.get("accounts", [])
|
||||
if accounts and all(a.get("auth_expired") for a in accounts):
|
||||
logger.warning("All Google Calendar accounts have expired authentication")
|
||||
logger.warning(
|
||||
"All Google Calendar accounts have expired authentication"
|
||||
)
|
||||
return {
|
||||
"status": "auth_error",
|
||||
"message": "All connected Google Calendar accounts need re-authentication. Please re-authenticate in your connector settings.",
|
||||
|
|
@ -194,7 +196,10 @@ def create_create_calendar_event_tool(
|
|||
f"Creating calendar event: summary='{final_summary}', connector={actual_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -216,7 +221,9 @@ def create_create_calendar_event_tool(
|
|||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||
for key in ("token", "refresh_token", "client_secret"):
|
||||
if config_data.get(key):
|
||||
config_data[key] = token_encryption.decrypt_token(config_data[key])
|
||||
config_data[key] = token_encryption.decrypt_token(
|
||||
config_data[key]
|
||||
)
|
||||
|
||||
exp = config_data.get("expiry", "")
|
||||
if exp:
|
||||
|
|
@ -254,9 +261,11 @@ def create_create_calendar_event_tool(
|
|||
try:
|
||||
created = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: service.events()
|
||||
.insert(calendarId="primary", body=event_body)
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.events()
|
||||
.insert(calendarId="primary", body=event_body)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
|
|||
|
|
@ -187,7 +187,10 @@ def create_delete_calendar_event_tool(
|
|||
f"Deleting calendar event: event_id='{final_event_id}', connector={actual_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -209,7 +212,9 @@ def create_delete_calendar_event_tool(
|
|||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||
for key in ("token", "refresh_token", "client_secret"):
|
||||
if config_data.get(key):
|
||||
config_data[key] = token_encryption.decrypt_token(config_data[key])
|
||||
config_data[key] = token_encryption.decrypt_token(
|
||||
config_data[key]
|
||||
)
|
||||
|
||||
exp = config_data.get("expiry", "")
|
||||
if exp:
|
||||
|
|
@ -232,9 +237,11 @@ def create_delete_calendar_event_tool(
|
|||
try:
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: service.events()
|
||||
.delete(calendarId="primary", eventId=final_event_id)
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.events()
|
||||
.delete(calendarId="primary", eventId=final_event_id)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
@ -269,9 +276,7 @@ def create_delete_calendar_event_tool(
|
|||
}
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"Calendar event deleted: event_id={final_event_id}"
|
||||
)
|
||||
logger.info(f"Calendar event deleted: event_id={final_event_id}")
|
||||
|
||||
delete_result: dict[str, Any] = {
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -58,9 +58,7 @@ def create_update_calendar_event_tool(
|
|||
- "Reschedule the team standup to 3pm"
|
||||
- "Change the location of my dentist appointment"
|
||||
"""
|
||||
logger.info(
|
||||
f"update_calendar_event called: event_ref='{event_title_or_id}'"
|
||||
)
|
||||
logger.info(f"update_calendar_event called: event_ref='{event_title_or_id}'")
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {
|
||||
|
|
@ -83,9 +81,7 @@ def create_update_calendar_event_tool(
|
|||
return {"status": "error", "message": error_msg}
|
||||
|
||||
if context.get("auth_expired"):
|
||||
logger.warning(
|
||||
"Google Calendar account has expired authentication"
|
||||
)
|
||||
logger.warning("Google Calendar account has expired authentication")
|
||||
return {
|
||||
"status": "auth_error",
|
||||
"message": "The Google Calendar account for this event needs re-authentication. Please re-authenticate in your connector settings.",
|
||||
|
|
@ -162,8 +158,12 @@ def create_update_calendar_event_tool(
|
|||
"connector_id", connector_id_from_context
|
||||
)
|
||||
final_new_summary = final_params.get("new_summary", new_summary)
|
||||
final_new_start_datetime = final_params.get("new_start_datetime", new_start_datetime)
|
||||
final_new_end_datetime = final_params.get("new_end_datetime", new_end_datetime)
|
||||
final_new_start_datetime = final_params.get(
|
||||
"new_start_datetime", new_start_datetime
|
||||
)
|
||||
final_new_end_datetime = final_params.get(
|
||||
"new_end_datetime", new_end_datetime
|
||||
)
|
||||
final_new_description = final_params.get("new_description", new_description)
|
||||
final_new_location = final_params.get("new_location", new_location)
|
||||
final_new_attendees = final_params.get("new_attendees", new_attendees)
|
||||
|
|
@ -204,7 +204,10 @@ def create_update_calendar_event_tool(
|
|||
f"Updating calendar event: event_id='{final_event_id}', connector={actual_connector_id}"
|
||||
)
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -226,7 +229,9 @@ def create_update_calendar_event_tool(
|
|||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||
for key in ("token", "refresh_token", "client_secret"):
|
||||
if config_data.get(key):
|
||||
config_data[key] = token_encryption.decrypt_token(config_data[key])
|
||||
config_data[key] = token_encryption.decrypt_token(
|
||||
config_data[key]
|
||||
)
|
||||
|
||||
exp = config_data.get("expiry", "")
|
||||
if exp:
|
||||
|
|
@ -250,11 +255,25 @@ def create_update_calendar_event_tool(
|
|||
if final_new_summary is not None:
|
||||
update_body["summary"] = final_new_summary
|
||||
if final_new_start_datetime is not None:
|
||||
tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC"
|
||||
update_body["start"] = {"dateTime": final_new_start_datetime, "timeZone": tz}
|
||||
tz = (
|
||||
context.get("timezone", "UTC")
|
||||
if isinstance(context, dict)
|
||||
else "UTC"
|
||||
)
|
||||
update_body["start"] = {
|
||||
"dateTime": final_new_start_datetime,
|
||||
"timeZone": tz,
|
||||
}
|
||||
if final_new_end_datetime is not None:
|
||||
tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC"
|
||||
update_body["end"] = {"dateTime": final_new_end_datetime, "timeZone": tz}
|
||||
tz = (
|
||||
context.get("timezone", "UTC")
|
||||
if isinstance(context, dict)
|
||||
else "UTC"
|
||||
)
|
||||
update_body["end"] = {
|
||||
"dateTime": final_new_end_datetime,
|
||||
"timeZone": tz,
|
||||
}
|
||||
if final_new_description is not None:
|
||||
update_body["description"] = final_new_description
|
||||
if final_new_location is not None:
|
||||
|
|
@ -273,9 +292,15 @@ def create_update_calendar_event_tool(
|
|||
try:
|
||||
updated = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: service.events()
|
||||
.patch(calendarId="primary", eventId=final_event_id, body=update_body)
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.events()
|
||||
.patch(
|
||||
calendarId="primary",
|
||||
eventId=final_event_id,
|
||||
body=update_body,
|
||||
)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
except Exception as api_err:
|
||||
from googleapiclient.errors import HttpError
|
||||
|
|
@ -310,9 +335,7 @@ def create_update_calendar_event_tool(
|
|||
}
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"Calendar event updated: event_id={final_event_id}"
|
||||
)
|
||||
logger.info(f"Calendar event updated: event_id={final_event_id}")
|
||||
|
||||
kb_message_suffix = ""
|
||||
if document_id is not None:
|
||||
|
|
@ -328,7 +351,9 @@ def create_update_calendar_event_tool(
|
|||
user_id=user_id,
|
||||
)
|
||||
if kb_result["status"] == "success":
|
||||
kb_message_suffix = " Your knowledge base has also been updated."
|
||||
kb_message_suffix = (
|
||||
" Your knowledge base has also been updated."
|
||||
)
|
||||
else:
|
||||
kb_message_suffix = " The knowledge base will be updated in the next scheduled sync."
|
||||
except Exception as kb_err:
|
||||
|
|
|
|||
|
|
@ -208,7 +208,10 @@ def create_create_google_drive_file_tool(
|
|||
)
|
||||
|
||||
pre_built_creds = None
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
|
|||
|
|
@ -187,7 +187,10 @@ def create_delete_google_drive_file_tool(
|
|||
)
|
||||
|
||||
pre_built_creds = None
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
):
|
||||
from app.utils.google_credentials import build_composio_credentials
|
||||
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
|
|
@ -210,7 +213,10 @@ def create_delete_google_drive_file_tool(
|
|||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
if not connector.config.get("auth_expired"):
|
||||
connector.config = {**connector.config, "auth_expired": True}
|
||||
connector.config = {
|
||||
**connector.config,
|
||||
"auth_expired": True,
|
||||
}
|
||||
flag_modified(connector, "config")
|
||||
await db_session.commit()
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -45,14 +45,18 @@ def create_create_jira_issue_tool(
|
|||
- If status is "rejected", the user declined. Do NOT retry.
|
||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
||||
"""
|
||||
logger.info(f"create_jira_issue called: project_key='{project_key}', summary='{summary}'")
|
||||
logger.info(
|
||||
f"create_jira_issue called: project_key='{project_key}', summary='{summary}'"
|
||||
)
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||
|
||||
try:
|
||||
metadata_service = JiraToolMetadataService(db_session)
|
||||
context = await metadata_service.get_creation_context(search_space_id, user_id)
|
||||
context = await metadata_service.get_creation_context(
|
||||
search_space_id, user_id
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
return {"status": "error", "message": context["error"]}
|
||||
|
|
@ -65,24 +69,30 @@ def create_create_jira_issue_tool(
|
|||
"connector_type": "jira",
|
||||
}
|
||||
|
||||
approval = interrupt({
|
||||
"type": "jira_issue_creation",
|
||||
"action": {
|
||||
"tool": "create_jira_issue",
|
||||
"params": {
|
||||
"project_key": project_key,
|
||||
"summary": summary,
|
||||
"issue_type": issue_type,
|
||||
"description": description,
|
||||
"priority": priority,
|
||||
"connector_id": connector_id,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "jira_issue_creation",
|
||||
"action": {
|
||||
"tool": "create_jira_issue",
|
||||
"params": {
|
||||
"project_key": project_key,
|
||||
"summary": summary,
|
||||
"issue_type": issue_type,
|
||||
"description": description,
|
||||
"priority": priority,
|
||||
"connector_id": connector_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -91,7 +101,10 @@ def create_create_jira_issue_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The issue was not created."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The issue was not created.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -123,7 +136,8 @@ def create_create_jira_issue_tool(
|
|||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
|
|
@ -136,15 +150,21 @@ def create_create_jira_issue_tool(
|
|||
SearchSourceConnector.id == actual_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Jira connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Jira connector is invalid.",
|
||||
}
|
||||
|
||||
try:
|
||||
jira_history = JiraHistoryConnector(session=db_session, connector_id=actual_connector_id)
|
||||
jira_history = JiraHistoryConnector(
|
||||
session=db_session, connector_id=actual_connector_id
|
||||
)
|
||||
jira_client = await jira_history._get_jira_client()
|
||||
api_result = await asyncio.to_thread(
|
||||
jira_client.create_issue,
|
||||
|
|
@ -175,6 +195,7 @@ def create_create_jira_issue_tool(
|
|||
kb_message_suffix = ""
|
||||
try:
|
||||
from app.services.jira import JiraKBSyncService
|
||||
|
||||
kb_service = JiraKBSyncService(db_session)
|
||||
kb_result = await kb_service.sync_after_create(
|
||||
issue_id=issue_key,
|
||||
|
|
@ -202,9 +223,13 @@ def create_create_jira_issue_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error creating Jira issue: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while creating the issue."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while creating the issue.",
|
||||
}
|
||||
|
||||
return create_jira_issue
|
||||
|
|
|
|||
|
|
@ -40,14 +40,18 @@ def create_delete_jira_issue_tool(
|
|||
- If status is "not_found", relay the message to the user.
|
||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
||||
"""
|
||||
logger.info(f"delete_jira_issue called: issue_title_or_key='{issue_title_or_key}'")
|
||||
logger.info(
|
||||
f"delete_jira_issue called: issue_title_or_key='{issue_title_or_key}'"
|
||||
)
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||
|
||||
try:
|
||||
metadata_service = JiraToolMetadataService(db_session)
|
||||
context = await metadata_service.get_deletion_context(search_space_id, user_id, issue_title_or_key)
|
||||
context = await metadata_service.get_deletion_context(
|
||||
search_space_id, user_id, issue_title_or_key
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
error_msg = context["error"]
|
||||
|
|
@ -67,21 +71,27 @@ def create_delete_jira_issue_tool(
|
|||
document_id = issue_data["document_id"]
|
||||
connector_id_from_context = context.get("account", {}).get("id")
|
||||
|
||||
approval = interrupt({
|
||||
"type": "jira_issue_deletion",
|
||||
"action": {
|
||||
"tool": "delete_jira_issue",
|
||||
"params": {
|
||||
"issue_key": issue_key,
|
||||
"connector_id": connector_id_from_context,
|
||||
"delete_from_kb": delete_from_kb,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "jira_issue_deletion",
|
||||
"action": {
|
||||
"tool": "delete_jira_issue",
|
||||
"params": {
|
||||
"issue_key": issue_key,
|
||||
"connector_id": connector_id_from_context,
|
||||
"delete_from_kb": delete_from_kb,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -90,7 +100,10 @@ def create_delete_jira_issue_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The issue was not deleted."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The issue was not deleted.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -102,29 +115,40 @@ def create_delete_jira_issue_tool(
|
|||
final_params = decision["args"]
|
||||
|
||||
final_issue_key = final_params.get("issue_key", issue_key)
|
||||
final_connector_id = final_params.get("connector_id", connector_id_from_context)
|
||||
final_connector_id = final_params.get(
|
||||
"connector_id", connector_id_from_context
|
||||
)
|
||||
final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb)
|
||||
|
||||
from sqlalchemy.future import select
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||
|
||||
if not final_connector_id:
|
||||
return {"status": "error", "message": "No connector found for this issue."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No connector found for this issue.",
|
||||
}
|
||||
|
||||
result = await db_session.execute(
|
||||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.id == final_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Jira connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Jira connector is invalid.",
|
||||
}
|
||||
|
||||
try:
|
||||
jira_history = JiraHistoryConnector(session=db_session, connector_id=final_connector_id)
|
||||
jira_history = JiraHistoryConnector(
|
||||
session=db_session, connector_id=final_connector_id
|
||||
)
|
||||
jira_client = await jira_history._get_jira_client()
|
||||
await asyncio.to_thread(jira_client.delete_issue, final_issue_key)
|
||||
except Exception as api_err:
|
||||
|
|
@ -146,6 +170,7 @@ def create_delete_jira_issue_tool(
|
|||
if final_delete_from_kb and document_id:
|
||||
try:
|
||||
from app.db import Document
|
||||
|
||||
doc_result = await db_session.execute(
|
||||
select(Document).filter(Document.id == document_id)
|
||||
)
|
||||
|
|
@ -171,9 +196,13 @@ def create_delete_jira_issue_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error deleting Jira issue: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while deleting the issue."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while deleting the issue.",
|
||||
}
|
||||
|
||||
return delete_jira_issue
|
||||
|
|
|
|||
|
|
@ -44,14 +44,18 @@ def create_update_jira_issue_tool(
|
|||
- If status is "not_found", relay the message and ask user to verify.
|
||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
||||
"""
|
||||
logger.info(f"update_jira_issue called: issue_title_or_key='{issue_title_or_key}'")
|
||||
logger.info(
|
||||
f"update_jira_issue called: issue_title_or_key='{issue_title_or_key}'"
|
||||
)
|
||||
|
||||
if db_session is None or search_space_id is None or user_id is None:
|
||||
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||
|
||||
try:
|
||||
metadata_service = JiraToolMetadataService(db_session)
|
||||
context = await metadata_service.get_update_context(search_space_id, user_id, issue_title_or_key)
|
||||
context = await metadata_service.get_update_context(
|
||||
search_space_id, user_id, issue_title_or_key
|
||||
)
|
||||
|
||||
if "error" in context:
|
||||
error_msg = context["error"]
|
||||
|
|
@ -71,24 +75,30 @@ def create_update_jira_issue_tool(
|
|||
document_id = issue_data.get("document_id")
|
||||
connector_id_from_context = context.get("account", {}).get("id")
|
||||
|
||||
approval = interrupt({
|
||||
"type": "jira_issue_update",
|
||||
"action": {
|
||||
"tool": "update_jira_issue",
|
||||
"params": {
|
||||
"issue_key": issue_key,
|
||||
"document_id": document_id,
|
||||
"new_summary": new_summary,
|
||||
"new_description": new_description,
|
||||
"new_priority": new_priority,
|
||||
"connector_id": connector_id_from_context,
|
||||
approval = interrupt(
|
||||
{
|
||||
"type": "jira_issue_update",
|
||||
"action": {
|
||||
"tool": "update_jira_issue",
|
||||
"params": {
|
||||
"issue_key": issue_key,
|
||||
"document_id": document_id,
|
||||
"new_summary": new_summary,
|
||||
"new_description": new_description,
|
||||
"new_priority": new_priority,
|
||||
"connector_id": connector_id_from_context,
|
||||
},
|
||||
},
|
||||
},
|
||||
"context": context,
|
||||
})
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
|
||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
decisions_raw = (
|
||||
approval.get("decisions", []) if isinstance(approval, dict) else []
|
||||
)
|
||||
decisions = (
|
||||
decisions_raw if isinstance(decisions_raw, list) else [decisions_raw]
|
||||
)
|
||||
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||
if not decisions:
|
||||
return {"status": "error", "message": "No approval decision received"}
|
||||
|
|
@ -97,7 +107,10 @@ def create_update_jira_issue_tool(
|
|||
decision_type = decision.get("type") or decision.get("decision_type")
|
||||
|
||||
if decision_type == "reject":
|
||||
return {"status": "rejected", "message": "User declined. The issue was not updated."}
|
||||
return {
|
||||
"status": "rejected",
|
||||
"message": "User declined. The issue was not updated.",
|
||||
}
|
||||
|
||||
final_params: dict[str, Any] = {}
|
||||
edited_action = decision.get("edited_action")
|
||||
|
|
@ -112,26 +125,35 @@ def create_update_jira_issue_tool(
|
|||
final_summary = final_params.get("new_summary", new_summary)
|
||||
final_description = final_params.get("new_description", new_description)
|
||||
final_priority = final_params.get("new_priority", new_priority)
|
||||
final_connector_id = final_params.get("connector_id", connector_id_from_context)
|
||||
final_connector_id = final_params.get(
|
||||
"connector_id", connector_id_from_context
|
||||
)
|
||||
final_document_id = final_params.get("document_id", document_id)
|
||||
|
||||
from sqlalchemy.future import select
|
||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||
|
||||
if not final_connector_id:
|
||||
return {"status": "error", "message": "No connector found for this issue."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No connector found for this issue.",
|
||||
}
|
||||
|
||||
result = await db_session.execute(
|
||||
select(SearchSourceConnector).filter(
|
||||
SearchSourceConnector.id == final_connector_id,
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
SearchSourceConnector.connector_type
|
||||
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||
)
|
||||
)
|
||||
connector = result.scalars().first()
|
||||
if not connector:
|
||||
return {"status": "error", "message": "Selected Jira connector is invalid."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Selected Jira connector is invalid.",
|
||||
}
|
||||
|
||||
fields: dict[str, Any] = {}
|
||||
if final_summary:
|
||||
|
|
@ -140,7 +162,12 @@ def create_update_jira_issue_tool(
|
|||
fields["description"] = {
|
||||
"type": "doc",
|
||||
"version": 1,
|
||||
"content": [{"type": "paragraph", "content": [{"type": "text", "text": final_description}]}],
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{"type": "text", "text": final_description}],
|
||||
}
|
||||
],
|
||||
}
|
||||
if final_priority:
|
||||
fields["priority"] = {"name": final_priority}
|
||||
|
|
@ -149,9 +176,13 @@ def create_update_jira_issue_tool(
|
|||
return {"status": "error", "message": "No changes specified."}
|
||||
|
||||
try:
|
||||
jira_history = JiraHistoryConnector(session=db_session, connector_id=final_connector_id)
|
||||
jira_history = JiraHistoryConnector(
|
||||
session=db_session, connector_id=final_connector_id
|
||||
)
|
||||
jira_client = await jira_history._get_jira_client()
|
||||
await asyncio.to_thread(jira_client.update_issue, final_issue_key, fields)
|
||||
await asyncio.to_thread(
|
||||
jira_client.update_issue, final_issue_key, fields
|
||||
)
|
||||
except Exception as api_err:
|
||||
if "status code 403" in str(api_err).lower():
|
||||
try:
|
||||
|
|
@ -171,6 +202,7 @@ def create_update_jira_issue_tool(
|
|||
if final_document_id:
|
||||
try:
|
||||
from app.services.jira import JiraKBSyncService
|
||||
|
||||
kb_service = JiraKBSyncService(db_session)
|
||||
kb_result = await kb_service.sync_after_update(
|
||||
document_id=final_document_id,
|
||||
|
|
@ -179,12 +211,18 @@ def create_update_jira_issue_tool(
|
|||
search_space_id=search_space_id,
|
||||
)
|
||||
if kb_result["status"] == "success":
|
||||
kb_message_suffix = " Your knowledge base has also been updated."
|
||||
kb_message_suffix = (
|
||||
" Your knowledge base has also been updated."
|
||||
)
|
||||
else:
|
||||
kb_message_suffix = " The knowledge base will be updated in the next sync."
|
||||
kb_message_suffix = (
|
||||
" The knowledge base will be updated in the next sync."
|
||||
)
|
||||
except Exception as kb_err:
|
||||
logger.warning(f"KB sync after update failed: {kb_err}")
|
||||
kb_message_suffix = " The knowledge base will be updated in the next sync."
|
||||
kb_message_suffix = (
|
||||
" The knowledge base will be updated in the next sync."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
|
@ -194,9 +232,13 @@ def create_update_jira_issue_tool(
|
|||
|
||||
except Exception as e:
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
if isinstance(e, GraphInterrupt):
|
||||
raise
|
||||
logger.error(f"Error updating Jira issue: {e}", exc_info=True)
|
||||
return {"status": "error", "message": "Something went wrong while updating the issue."}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Something went wrong while updating the issue.",
|
||||
}
|
||||
|
||||
return update_jira_issue
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ _DEGENERATE_QUERY_RE = re.compile(
|
|||
# a real search. We want breadth (many docs) over depth (many chunks).
|
||||
_BROWSE_MAX_CHUNKS_PER_DOC = 5
|
||||
|
||||
|
||||
def _is_degenerate_query(query: str) -> bool:
|
||||
"""Return True when the query carries no meaningful search signal.
|
||||
|
||||
|
|
@ -82,7 +83,9 @@ async def _browse_recent_documents(
|
|||
base_conditions = [Document.search_space_id == search_space_id]
|
||||
|
||||
if document_type is not None:
|
||||
type_list = document_type if isinstance(document_type, list) else [document_type]
|
||||
type_list = (
|
||||
document_type if isinstance(document_type, list) else [document_type]
|
||||
)
|
||||
doc_type_enums = []
|
||||
for dt in type_list:
|
||||
if isinstance(dt, str):
|
||||
|
|
|
|||
|
|
@ -245,7 +245,9 @@ def create_create_notion_page_tool(
|
|||
user_id=user_id,
|
||||
)
|
||||
if kb_result["status"] == "success":
|
||||
kb_message_suffix = " Your knowledge base has also been updated."
|
||||
kb_message_suffix = (
|
||||
" Your knowledge base has also been updated."
|
||||
)
|
||||
else:
|
||||
kb_message_suffix = " This page will be added to your knowledge base in the next scheduled sync."
|
||||
except Exception as kb_err:
|
||||
|
|
|
|||
|
|
@ -280,7 +280,9 @@ def create_delete_notion_page_tool(
|
|||
return {
|
||||
"status": "auth_error",
|
||||
"message": str(e),
|
||||
"connector_id": connector_id_from_context if "connector_id_from_context" in dir() else None,
|
||||
"connector_id": connector_id_from_context
|
||||
if "connector_id_from_context" in dir()
|
||||
else None,
|
||||
"connector_type": "notion",
|
||||
}
|
||||
if isinstance(e, ValueError | NotionAPIError):
|
||||
|
|
|
|||
|
|
@ -281,7 +281,9 @@ def create_update_notion_page_tool(
|
|||
return {
|
||||
"status": "auth_error",
|
||||
"message": str(e),
|
||||
"connector_id": connector_id_from_context if "connector_id_from_context" in dir() else None,
|
||||
"connector_id": connector_id_from_context
|
||||
if "connector_id_from_context" in dir()
|
||||
else None,
|
||||
"connector_type": "notion",
|
||||
}
|
||||
if isinstance(e, ValueError | NotionAPIError):
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ if config.NEXT_FRONTEND_URL:
|
|||
allowed_origins.append(www_url)
|
||||
|
||||
allowed_origins.extend(
|
||||
[ # For local development and desktop app
|
||||
[ # For local development and desktop app
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -190,7 +190,9 @@ class ConfluenceHistoryConnector:
|
|||
)
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
from app.routes.confluence_add_connector_route import refresh_confluence_token
|
||||
from app.routes.confluence_add_connector_route import (
|
||||
refresh_confluence_token,
|
||||
)
|
||||
|
||||
connector = await refresh_confluence_token(self._session, connector)
|
||||
|
||||
|
|
@ -375,13 +377,9 @@ class ConfluenceHistoryConnector:
|
|||
url, headers=headers, json=json_payload, params=params
|
||||
)
|
||||
elif method_upper == "DELETE":
|
||||
response = await http_client.delete(
|
||||
url, headers=headers, params=params
|
||||
)
|
||||
response = await http_client.delete(url, headers=headers, params=params)
|
||||
else:
|
||||
response = await http_client.get(
|
||||
url, headers=headers, params=params
|
||||
)
|
||||
response = await http_client.get(url, headers=headers, params=params)
|
||||
|
||||
response.raise_for_status()
|
||||
if response.status_code == 204 or not response.text:
|
||||
|
|
|
|||
|
|
@ -60,9 +60,7 @@ class GoogleCalendarConnector:
|
|||
has_standard_refresh = bool(self._credentials.refresh_token)
|
||||
|
||||
if has_standard_refresh:
|
||||
if not all(
|
||||
[self._credentials.client_id, self._credentials.client_secret]
|
||||
):
|
||||
if not all([self._credentials.client_id, self._credentials.client_secret]):
|
||||
raise ValueError(
|
||||
"Google OAuth credentials (client_id, client_secret) must be set"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -89,9 +89,7 @@ class GoogleGmailConnector:
|
|||
has_standard_refresh = bool(self._credentials.refresh_token)
|
||||
|
||||
if has_standard_refresh:
|
||||
if not all(
|
||||
[self._credentials.client_id, self._credentials.client_secret]
|
||||
):
|
||||
if not all([self._credentials.client_id, self._credentials.client_secret]):
|
||||
raise ValueError(
|
||||
"Google OAuth credentials (client_id, client_secret) must be set"
|
||||
)
|
||||
|
|
@ -139,17 +137,13 @@ class GoogleGmailConnector:
|
|||
from app.utils.oauth_security import TokenEncryption
|
||||
|
||||
creds_dict = json.loads(self._credentials.to_json())
|
||||
token_encrypted = connector.config.get(
|
||||
"_token_encrypted", False
|
||||
)
|
||||
token_encrypted = connector.config.get("_token_encrypted", False)
|
||||
|
||||
if token_encrypted and config.SECRET_KEY:
|
||||
token_encryption = TokenEncryption(config.SECRET_KEY)
|
||||
if creds_dict.get("token"):
|
||||
creds_dict["token"] = (
|
||||
token_encryption.encrypt_token(
|
||||
creds_dict["token"]
|
||||
)
|
||||
creds_dict["token"] = token_encryption.encrypt_token(
|
||||
creds_dict["token"]
|
||||
)
|
||||
if creds_dict.get("refresh_token"):
|
||||
creds_dict["refresh_token"] = (
|
||||
|
|
|
|||
|
|
@ -219,7 +219,9 @@ class ChucksHybridSearchRetriever:
|
|||
|
||||
# Add document type filter if provided (single string or list of strings)
|
||||
if document_type is not None:
|
||||
type_list = document_type if isinstance(document_type, list) else [document_type]
|
||||
type_list = (
|
||||
document_type if isinstance(document_type, list) else [document_type]
|
||||
)
|
||||
doc_type_enums = []
|
||||
for dt in type_list:
|
||||
if isinstance(dt, str):
|
||||
|
|
|
|||
|
|
@ -199,7 +199,9 @@ class DocumentHybridSearchRetriever:
|
|||
|
||||
# Add document type filter if provided (single string or list of strings)
|
||||
if document_type is not None:
|
||||
type_list = document_type if isinstance(document_type, list) else [document_type]
|
||||
type_list = (
|
||||
document_type if isinstance(document_type, list) else [document_type]
|
||||
)
|
||||
doc_type_enums = []
|
||||
for dt in type_list:
|
||||
if isinstance(dt, str):
|
||||
|
|
|
|||
|
|
@ -461,10 +461,14 @@ async def reauth_composio_connector(
|
|||
return_url: Optional frontend path to redirect to after completion
|
||||
"""
|
||||
if not ComposioService.is_enabled():
|
||||
raise HTTPException(status_code=503, detail="Composio integration is not enabled.")
|
||||
raise HTTPException(
|
||||
status_code=503, detail="Composio integration is not enabled."
|
||||
)
|
||||
|
||||
if not config.SECRET_KEY:
|
||||
raise HTTPException(status_code=500, detail="SECRET_KEY not configured for OAuth security.")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="SECRET_KEY not configured for OAuth security."
|
||||
)
|
||||
|
||||
try:
|
||||
result = await session.execute(
|
||||
|
|
@ -502,7 +506,9 @@ async def reauth_composio_connector(
|
|||
callback_base = config.COMPOSIO_REDIRECT_URI
|
||||
if not callback_base:
|
||||
backend_url = config.BACKEND_URL or "http://localhost:8000"
|
||||
callback_base = f"{backend_url}/api/v1/auth/composio/connector/reauth/callback"
|
||||
callback_base = (
|
||||
f"{backend_url}/api/v1/auth/composio/connector/reauth/callback"
|
||||
)
|
||||
else:
|
||||
# Replace the normal callback path with the reauth one
|
||||
callback_base = callback_base.replace(
|
||||
|
|
@ -524,8 +530,13 @@ async def reauth_composio_connector(
|
|||
connector.config = {**connector.config, "auth_expired": False}
|
||||
flag_modified(connector, "config")
|
||||
await session.commit()
|
||||
logger.info(f"Composio account {connected_account_id} refreshed server-side (no redirect needed)")
|
||||
return {"success": True, "message": "Authentication refreshed successfully."}
|
||||
logger.info(
|
||||
f"Composio account {connected_account_id} refreshed server-side (no redirect needed)"
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Authentication refreshed successfully.",
|
||||
}
|
||||
|
||||
logger.info(f"Initiating Composio re-auth for connector {connector_id}")
|
||||
return {"auth_url": refresh_result["redirect_url"]}
|
||||
|
|
@ -679,9 +690,7 @@ async def list_composio_drive_folders(
|
|||
)
|
||||
|
||||
credentials = build_composio_credentials(composio_connected_account_id)
|
||||
drive_client = GoogleDriveClient(
|
||||
session, connector_id, credentials=credentials
|
||||
)
|
||||
drive_client = GoogleDriveClient(session, connector_id, credentials=credentials)
|
||||
|
||||
items, error = await list_folder_contents(drive_client, parent_id=parent_id)
|
||||
|
||||
|
|
@ -699,11 +708,17 @@ async def list_composio_drive_folders(
|
|||
connector.config = {**connector.config, "auth_expired": True}
|
||||
flag_modified(connector, "config")
|
||||
await session.commit()
|
||||
logger.info(f"Marked Composio connector {connector_id} as auth_expired")
|
||||
logger.info(
|
||||
f"Marked Composio connector {connector_id} as auth_expired"
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Failed to persist auth_expired for connector {connector_id}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Google Drive authentication expired. Please re-authenticate."
|
||||
status_code=400,
|
||||
detail="Google Drive authentication expired. Please re-authenticate.",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to list folder contents: {error}"
|
||||
|
|
@ -736,11 +751,17 @@ async def list_composio_drive_folders(
|
|||
connector.config = {**connector.config, "auth_expired": True}
|
||||
flag_modified(connector, "config")
|
||||
await session.commit()
|
||||
logger.info(f"Marked Composio connector {connector_id} as auth_expired")
|
||||
logger.info(
|
||||
f"Marked Composio connector {connector_id} as auth_expired"
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Failed to persist auth_expired for connector {connector_id}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Google Drive authentication expired. Please re-authenticate."
|
||||
status_code=400,
|
||||
detail="Google Drive authentication expired. Please re-authenticate.",
|
||||
) from e
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to list Drive contents: {e!s}"
|
||||
|
|
|
|||
|
|
@ -520,9 +520,13 @@ async def list_google_drive_folders(
|
|||
await session.commit()
|
||||
logger.info(f"Marked connector {connector_id} as auth_expired")
|
||||
except Exception:
|
||||
logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Failed to persist auth_expired for connector {connector_id}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Google Drive authentication expired. Please re-authenticate."
|
||||
status_code=400,
|
||||
detail="Google Drive authentication expired. Please re-authenticate.",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to list folder contents: {error}"
|
||||
|
|
@ -562,9 +566,13 @@ async def list_google_drive_folders(
|
|||
await session.commit()
|
||||
logger.info(f"Marked connector {connector_id} as auth_expired")
|
||||
except Exception:
|
||||
logger.warning(f"Failed to persist auth_expired for connector {connector_id}", exc_info=True)
|
||||
logger.warning(
|
||||
f"Failed to persist auth_expired for connector {connector_id}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Google Drive authentication expired. Please re-authenticate."
|
||||
status_code=400,
|
||||
detail="Google Drive authentication expired. Please re-authenticate.",
|
||||
) from e
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to list Drive contents: {e!s}"
|
||||
|
|
|
|||
|
|
@ -580,7 +580,9 @@ async def refresh_linear_token(
|
|||
credentials_dict = credentials.to_dict()
|
||||
credentials_dict["_token_encrypted"] = True
|
||||
if connector.config.get("organization_name"):
|
||||
credentials_dict["organization_name"] = connector.config["organization_name"]
|
||||
credentials_dict["organization_name"] = connector.config[
|
||||
"organization_name"
|
||||
]
|
||||
credentials_dict.pop("auth_expired", None)
|
||||
connector.config = credentials_dict
|
||||
flag_modified(connector, "config")
|
||||
|
|
|
|||
|
|
@ -2374,7 +2374,11 @@ async def run_google_drive_indexing(
|
|||
# Index each folder with indexing options
|
||||
for folder in items.folders:
|
||||
try:
|
||||
indexed_count, skipped_count, error_message = await index_google_drive_files(
|
||||
(
|
||||
indexed_count,
|
||||
skipped_count,
|
||||
error_message,
|
||||
) = await index_google_drive_files(
|
||||
session,
|
||||
connector_id,
|
||||
search_space_id,
|
||||
|
|
@ -2429,7 +2433,9 @@ async def run_google_drive_indexing(
|
|||
)
|
||||
if _is_auth_error(error_message):
|
||||
await _persist_auth_expired(session, connector_id)
|
||||
error_message = "Google Drive authentication expired. Please re-authenticate."
|
||||
error_message = (
|
||||
"Google Drive authentication expired. Please re-authenticate."
|
||||
)
|
||||
else:
|
||||
# Update notification to storing stage
|
||||
if notification:
|
||||
|
|
|
|||
|
|
@ -283,9 +283,7 @@ class ComposioService:
|
|||
timeout=timeout,
|
||||
)
|
||||
status = getattr(account, "status", "UNKNOWN")
|
||||
logger.info(
|
||||
f"Composio account {connected_account_id} is now {status}"
|
||||
)
|
||||
logger.info(f"Composio account {connected_account_id} is now {status}")
|
||||
return status
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ class ConfluenceKBSyncService:
|
|||
content_hash = unique_hash
|
||||
|
||||
user_llm = await get_user_long_context_llm(
|
||||
self.db_session, user_id, search_space_id, disable_streaming=True,
|
||||
self.db_session,
|
||||
user_id,
|
||||
search_space_id,
|
||||
disable_streaming=True,
|
||||
)
|
||||
|
||||
doc_metadata_for_summary = {
|
||||
|
|
@ -116,17 +119,26 @@ class ConfluenceKBSyncService:
|
|||
|
||||
logger.info(
|
||||
"KB sync after create succeeded: doc_id=%s, page=%s",
|
||||
document.id, page_title,
|
||||
document.id,
|
||||
page_title,
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "duplicate key value violates unique constraint" in error_str or "uniqueviolationerror" in error_str:
|
||||
if (
|
||||
"duplicate key value violates unique constraint" in error_str
|
||||
or "uniqueviolationerror" in error_str
|
||||
):
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": "Duplicate document detected"}
|
||||
|
||||
logger.error("KB sync after create failed for page %s: %s", page_title, e, exc_info=True)
|
||||
logger.error(
|
||||
"KB sync after create failed for page %s: %s",
|
||||
page_title,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
|
@ -215,11 +227,14 @@ class ConfluenceKBSyncService:
|
|||
|
||||
logger.info(
|
||||
"KB sync successful for document %s (%s)",
|
||||
document_id, page_title,
|
||||
document_id,
|
||||
page_title,
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("KB sync failed for document %s: %s", document_id, e, exc_info=True)
|
||||
logger.error(
|
||||
"KB sync failed for document %s: %s", document_id, e, exc_info=True
|
||||
)
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class ConfluenceToolMetadataService:
|
|||
|
||||
async def _check_account_health(self, connector: SearchSourceConnector) -> bool:
|
||||
"""Check if the Confluence connector auth is still valid.
|
||||
|
||||
|
||||
Returns True if auth is expired/invalid, False if healthy.
|
||||
"""
|
||||
try:
|
||||
|
|
@ -112,7 +112,7 @@ class ConfluenceToolMetadataService:
|
|||
|
||||
async def get_creation_context(self, search_space_id: int, user_id: str) -> dict:
|
||||
"""Return context needed to create a new Confluence page.
|
||||
|
||||
|
||||
Fetches all connected accounts, and for the first healthy one fetches spaces.
|
||||
"""
|
||||
connectors = await self._get_all_confluence_connectors(search_space_id, user_id)
|
||||
|
|
@ -126,10 +126,12 @@ class ConfluenceToolMetadataService:
|
|||
for connector in connectors:
|
||||
auth_expired = await self._check_account_health(connector)
|
||||
workspace = ConfluenceWorkspace.from_connector(connector)
|
||||
accounts.append({
|
||||
**workspace.to_dict(),
|
||||
"auth_expired": auth_expired,
|
||||
})
|
||||
accounts.append(
|
||||
{
|
||||
**workspace.to_dict(),
|
||||
"auth_expired": auth_expired,
|
||||
}
|
||||
)
|
||||
|
||||
if not auth_expired and not fetched_context:
|
||||
try:
|
||||
|
|
@ -146,7 +148,8 @@ class ConfluenceToolMetadataService:
|
|||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to fetch Confluence spaces for connector %s: %s",
|
||||
connector.id, e,
|
||||
connector.id,
|
||||
e,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -158,7 +161,7 @@ class ConfluenceToolMetadataService:
|
|||
self, search_space_id: int, user_id: str, page_ref: str
|
||||
) -> dict:
|
||||
"""Return context needed to update an indexed Confluence page.
|
||||
|
||||
|
||||
Resolves the page from KB, then fetches current content and version from API.
|
||||
"""
|
||||
document = await self._resolve_page(search_space_id, user_id, page_ref)
|
||||
|
|
@ -191,7 +194,11 @@ class ConfluenceToolMetadataService:
|
|||
await client.close()
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "401" in error_str or "403" in error_str or "authentication" in error_str:
|
||||
if (
|
||||
"401" in error_str
|
||||
or "403" in error_str
|
||||
or "authentication" in error_str
|
||||
):
|
||||
return {
|
||||
"error": f"Failed to fetch Confluence page: {e!s}",
|
||||
"auth_expired": True,
|
||||
|
|
@ -207,7 +214,9 @@ class ConfluenceToolMetadataService:
|
|||
body_storage = storage.get("value", "")
|
||||
|
||||
version_obj = page_data.get("version", {})
|
||||
version_number = version_obj.get("number", 1) if isinstance(version_obj, dict) else 1
|
||||
version_number = (
|
||||
version_obj.get("number", 1) if isinstance(version_obj, dict) else 1
|
||||
)
|
||||
|
||||
return {
|
||||
"account": {**workspace.to_dict(), "auth_expired": False},
|
||||
|
|
@ -263,9 +272,7 @@ class ConfluenceToolMetadataService:
|
|||
Document.document_type == DocumentType.CONFLUENCE_CONNECTOR,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
or_(
|
||||
func.lower(
|
||||
Document.document_metadata.op("->>")("page_title")
|
||||
)
|
||||
func.lower(Document.document_metadata.op("->>")("page_title"))
|
||||
== ref_lower,
|
||||
func.lower(Document.title) == ref_lower,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -183,10 +183,12 @@ class GmailToolMetadataService:
|
|||
and_(
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type.in_([
|
||||
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
|
||||
]),
|
||||
SearchSourceConnector.connector_type.in_(
|
||||
[
|
||||
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
||||
|
|
@ -223,9 +225,7 @@ class GmailToolMetadataService:
|
|||
service = build("gmail", "v1", credentials=creds)
|
||||
profile = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: service.users()
|
||||
.getProfile(userId="me")
|
||||
.execute(),
|
||||
lambda: service.users().getProfile(userId="me").execute(),
|
||||
)
|
||||
acc_dict["email"] = profile.get("emailAddress", "")
|
||||
except Exception:
|
||||
|
|
@ -306,10 +306,12 @@ class GmailToolMetadataService:
|
|||
|
||||
draft = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: service.users()
|
||||
.drafts()
|
||||
.get(userId="me", id=draft_id, format="full")
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.users()
|
||||
.drafts()
|
||||
.get(userId="me", id=draft_id, format="full")
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
|
||||
payload = draft.get("message", {}).get("payload", {})
|
||||
|
|
@ -422,15 +424,15 @@ class GmailToolMetadataService:
|
|||
.filter(
|
||||
and_(
|
||||
Document.search_space_id == search_space_id,
|
||||
Document.document_type.in_([
|
||||
DocumentType.GOOGLE_GMAIL_CONNECTOR,
|
||||
DocumentType.COMPOSIO_GMAIL_CONNECTOR,
|
||||
]),
|
||||
Document.document_type.in_(
|
||||
[
|
||||
DocumentType.GOOGLE_GMAIL_CONNECTOR,
|
||||
DocumentType.COMPOSIO_GMAIL_CONNECTOR,
|
||||
]
|
||||
),
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
or_(
|
||||
func.lower(
|
||||
cast(Document.document_metadata["subject"], String)
|
||||
)
|
||||
func.lower(cast(Document.document_metadata["subject"], String))
|
||||
== func.lower(email_ref),
|
||||
func.lower(Document.title) == func.lower(email_ref),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from app.db import Document, DocumentType, SearchSourceConnector, SearchSourceConnectorType
|
||||
from app.db import (
|
||||
Document,
|
||||
DocumentType,
|
||||
SearchSourceConnector,
|
||||
SearchSourceConnectorType,
|
||||
)
|
||||
from app.services.llm_service import get_user_long_context_llm
|
||||
from app.utils.document_converters import (
|
||||
create_document_chunks,
|
||||
|
|
@ -107,7 +112,9 @@ class GoogleCalendarKBSyncService:
|
|||
)
|
||||
else:
|
||||
logger.warning("No LLM configured -- using fallback summary")
|
||||
summary_content = f"Google Calendar Event: {event_summary}\n\n{indexable_content}"
|
||||
summary_content = (
|
||||
f"Google Calendar Event: {event_summary}\n\n{indexable_content}"
|
||||
)
|
||||
summary_embedding = embed_text(summary_content)
|
||||
|
||||
chunks = await create_document_chunks(indexable_content)
|
||||
|
|
@ -201,12 +208,16 @@ class GoogleCalendarKBSyncService:
|
|||
None, lambda: build("calendar", "v3", credentials=creds)
|
||||
)
|
||||
|
||||
calendar_id = (document.document_metadata or {}).get("calendar_id", "primary")
|
||||
calendar_id = (document.document_metadata or {}).get(
|
||||
"calendar_id", "primary"
|
||||
)
|
||||
live_event = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: service.events()
|
||||
.get(calendarId=calendar_id, eventId=event_id)
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.events()
|
||||
.get(calendarId=calendar_id, eventId=event_id)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
|
||||
event_summary = live_event.get("summary", "")
|
||||
|
|
@ -220,7 +231,10 @@ class GoogleCalendarKBSyncService:
|
|||
end_time = end_data.get("dateTime", end_data.get("date", ""))
|
||||
|
||||
attendees = [
|
||||
{"email": a.get("email", ""), "responseStatus": a.get("responseStatus", "")}
|
||||
{
|
||||
"email": a.get("email", ""),
|
||||
"responseStatus": a.get("responseStatus", ""),
|
||||
}
|
||||
for a in live_event.get("attendees", [])
|
||||
]
|
||||
|
||||
|
|
@ -252,7 +266,9 @@ class GoogleCalendarKBSyncService:
|
|||
indexable_content, user_llm, doc_metadata_for_summary
|
||||
)
|
||||
else:
|
||||
summary_content = f"Google Calendar Event: {event_summary}\n\n{indexable_content}"
|
||||
summary_content = (
|
||||
f"Google Calendar Event: {event_summary}\n\n{indexable_content}"
|
||||
)
|
||||
summary_embedding = embed_text(summary_content)
|
||||
|
||||
chunks = await create_document_chunks(indexable_content)
|
||||
|
|
@ -313,7 +329,10 @@ class GoogleCalendarKBSyncService:
|
|||
if not connector:
|
||||
raise ValueError(f"Connector {connector_id} not found")
|
||||
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
if cca_id:
|
||||
return build_composio_credentials(cca_id)
|
||||
|
|
@ -328,11 +347,17 @@ class GoogleCalendarKBSyncService:
|
|||
if token_encrypted and app_config.SECRET_KEY:
|
||||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||
if config_data.get("token"):
|
||||
config_data["token"] = token_encryption.decrypt_token(config_data["token"])
|
||||
config_data["token"] = token_encryption.decrypt_token(
|
||||
config_data["token"]
|
||||
)
|
||||
if config_data.get("refresh_token"):
|
||||
config_data["refresh_token"] = token_encryption.decrypt_token(config_data["refresh_token"])
|
||||
config_data["refresh_token"] = token_encryption.decrypt_token(
|
||||
config_data["refresh_token"]
|
||||
)
|
||||
if config_data.get("client_secret"):
|
||||
config_data["client_secret"] = token_encryption.decrypt_token(config_data["client_secret"])
|
||||
config_data["client_secret"] = token_encryption.decrypt_token(
|
||||
config_data["client_secret"]
|
||||
)
|
||||
|
||||
exp = config_data.get("expiry", "")
|
||||
if exp:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ class GoogleCalendarAccount:
|
|||
name: str
|
||||
|
||||
@classmethod
|
||||
def from_connector(cls, connector: SearchSourceConnector) -> "GoogleCalendarAccount":
|
||||
def from_connector(
|
||||
cls, connector: SearchSourceConnector
|
||||
) -> "GoogleCalendarAccount":
|
||||
return cls(id=connector.id, name=connector.name)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
|
|
@ -93,7 +95,10 @@ class GoogleCalendarToolMetadataService:
|
|||
self._db_session = db_session
|
||||
|
||||
async def _build_credentials(self, connector: SearchSourceConnector) -> Credentials:
|
||||
if connector.connector_type == SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
if (
|
||||
connector.connector_type
|
||||
== SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
cca_id = connector.config.get("composio_connected_account_id")
|
||||
if cca_id:
|
||||
return build_composio_credentials(cca_id)
|
||||
|
|
@ -108,11 +113,17 @@ class GoogleCalendarToolMetadataService:
|
|||
if token_encrypted and app_config.SECRET_KEY:
|
||||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||
if config_data.get("token"):
|
||||
config_data["token"] = token_encryption.decrypt_token(config_data["token"])
|
||||
config_data["token"] = token_encryption.decrypt_token(
|
||||
config_data["token"]
|
||||
)
|
||||
if config_data.get("refresh_token"):
|
||||
config_data["refresh_token"] = token_encryption.decrypt_token(config_data["refresh_token"])
|
||||
config_data["refresh_token"] = token_encryption.decrypt_token(
|
||||
config_data["refresh_token"]
|
||||
)
|
||||
if config_data.get("client_secret"):
|
||||
config_data["client_secret"] = token_encryption.decrypt_token(config_data["client_secret"])
|
||||
config_data["client_secret"] = token_encryption.decrypt_token(
|
||||
config_data["client_secret"]
|
||||
)
|
||||
|
||||
exp = config_data.get("expiry", "")
|
||||
if exp:
|
||||
|
|
@ -149,10 +160,12 @@ class GoogleCalendarToolMetadataService:
|
|||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
lambda: build("calendar", "v3", credentials=creds)
|
||||
.calendarList()
|
||||
.list(maxResults=1)
|
||||
.execute(),
|
||||
lambda: (
|
||||
build("calendar", "v3", credentials=creds)
|
||||
.calendarList()
|
||||
.list(maxResults=1)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
|
|
@ -252,11 +265,13 @@ class GoogleCalendarToolMetadataService:
|
|||
None, lambda: service.calendarList().list().execute()
|
||||
)
|
||||
for cal in cal_list.get("items", []):
|
||||
calendars.append({
|
||||
"id": cal.get("id", ""),
|
||||
"summary": cal.get("summary", ""),
|
||||
"primary": cal.get("primary", False),
|
||||
})
|
||||
calendars.append(
|
||||
{
|
||||
"id": cal.get("id", ""),
|
||||
"summary": cal.get("summary", ""),
|
||||
"primary": cal.get("primary", False),
|
||||
}
|
||||
)
|
||||
|
||||
tz_setting = await loop.run_in_executor(
|
||||
None,
|
||||
|
|
@ -314,23 +329,34 @@ class GoogleCalendarToolMetadataService:
|
|||
calendar_id = event.calendar_id or "primary"
|
||||
live_event = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: service.events()
|
||||
.get(calendarId=calendar_id, eventId=event.event_id)
|
||||
.execute(),
|
||||
lambda: (
|
||||
service.events()
|
||||
.get(calendarId=calendar_id, eventId=event.event_id)
|
||||
.execute()
|
||||
),
|
||||
)
|
||||
|
||||
event_dict["summary"] = live_event.get("summary", event_dict["summary"])
|
||||
event_dict["description"] = live_event.get("description", event_dict["description"])
|
||||
event_dict["description"] = live_event.get(
|
||||
"description", event_dict["description"]
|
||||
)
|
||||
event_dict["location"] = live_event.get("location", event_dict["location"])
|
||||
|
||||
start_data = live_event.get("start", {})
|
||||
event_dict["start"] = start_data.get("dateTime", start_data.get("date", event_dict["start"]))
|
||||
event_dict["start"] = start_data.get(
|
||||
"dateTime", start_data.get("date", event_dict["start"])
|
||||
)
|
||||
|
||||
end_data = live_event.get("end", {})
|
||||
event_dict["end"] = end_data.get("dateTime", end_data.get("date", event_dict["end"]))
|
||||
event_dict["end"] = end_data.get(
|
||||
"dateTime", end_data.get("date", event_dict["end"])
|
||||
)
|
||||
|
||||
event_dict["attendees"] = [
|
||||
{"email": a.get("email", ""), "responseStatus": a.get("responseStatus", "")}
|
||||
{
|
||||
"email": a.get("email", ""),
|
||||
"responseStatus": a.get("responseStatus", ""),
|
||||
}
|
||||
for a in live_event.get("attendees", [])
|
||||
]
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ class GoogleDriveKBSyncService:
|
|||
|
||||
indexable_content = (content or "").strip()
|
||||
if not indexable_content:
|
||||
indexable_content = f"Google Drive file: {file_name} (type: {mime_type})"
|
||||
indexable_content = (
|
||||
f"Google Drive file: {file_name} (type: {mime_type})"
|
||||
)
|
||||
|
||||
content_hash = generate_content_hash(indexable_content, search_space_id)
|
||||
|
||||
|
|
@ -93,7 +95,9 @@ class GoogleDriveKBSyncService:
|
|||
)
|
||||
else:
|
||||
logger.warning("No LLM configured — using fallback summary")
|
||||
summary_content = f"Google Drive File: {file_name}\n\n{indexable_content}"
|
||||
summary_content = (
|
||||
f"Google Drive File: {file_name}\n\n{indexable_content}"
|
||||
)
|
||||
summary_embedding = embed_text(summary_content)
|
||||
|
||||
chunks = await create_document_chunks(indexable_content)
|
||||
|
|
|
|||
|
|
@ -133,10 +133,12 @@ class GoogleDriveToolMetadataService:
|
|||
and_(
|
||||
SearchSourceConnector.id == document.connector_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type.in_([
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]),
|
||||
SearchSourceConnector.connector_type.in_(
|
||||
[
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -168,10 +170,12 @@ class GoogleDriveToolMetadataService:
|
|||
and_(
|
||||
SearchSourceConnector.search_space_id == search_space_id,
|
||||
SearchSourceConnector.user_id == user_id,
|
||||
SearchSourceConnector.connector_type.in_([
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]),
|
||||
SearchSourceConnector.connector_type.in_(
|
||||
[
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ class JiraKBSyncService:
|
|||
if existing:
|
||||
logger.info(
|
||||
"Document for Jira issue %s already exists (doc_id=%s), skipping",
|
||||
issue_identifier, existing.id,
|
||||
issue_identifier,
|
||||
existing.id,
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
|
|
@ -61,7 +62,9 @@ class JiraKBSyncService:
|
|||
if not indexable_content:
|
||||
indexable_content = f"Jira Issue {issue_identifier}: {issue_title}"
|
||||
|
||||
issue_content = f"# {issue_identifier}: {issue_title}\n\n{indexable_content}"
|
||||
issue_content = (
|
||||
f"# {issue_identifier}: {issue_title}\n\n{indexable_content}"
|
||||
)
|
||||
|
||||
content_hash = generate_content_hash(issue_content, search_space_id)
|
||||
|
||||
|
|
@ -73,7 +76,10 @@ class JiraKBSyncService:
|
|||
content_hash = unique_hash
|
||||
|
||||
user_llm = await get_user_long_context_llm(
|
||||
self.db_session, user_id, search_space_id, disable_streaming=True,
|
||||
self.db_session,
|
||||
user_id,
|
||||
search_space_id,
|
||||
disable_streaming=True,
|
||||
)
|
||||
|
||||
doc_metadata_for_summary = {
|
||||
|
|
@ -88,7 +94,9 @@ class JiraKBSyncService:
|
|||
issue_content, user_llm, doc_metadata_for_summary
|
||||
)
|
||||
else:
|
||||
summary_content = f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}"
|
||||
summary_content = (
|
||||
f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}"
|
||||
)
|
||||
summary_embedding = embed_text(summary_content)
|
||||
|
||||
chunks = await create_document_chunks(issue_content)
|
||||
|
|
@ -122,17 +130,26 @@ class JiraKBSyncService:
|
|||
|
||||
logger.info(
|
||||
"KB sync after create succeeded: doc_id=%s, issue=%s",
|
||||
document.id, issue_identifier,
|
||||
document.id,
|
||||
issue_identifier,
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "duplicate key value violates unique constraint" in error_str or "uniqueviolationerror" in error_str:
|
||||
if (
|
||||
"duplicate key value violates unique constraint" in error_str
|
||||
or "uniqueviolationerror" in error_str
|
||||
):
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": "Duplicate document detected"}
|
||||
|
||||
logger.error("KB sync after create failed for issue %s: %s", issue_identifier, e, exc_info=True)
|
||||
logger.error(
|
||||
"KB sync after create failed for issue %s: %s",
|
||||
issue_identifier,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
|
@ -189,14 +206,18 @@ class JiraKBSyncService:
|
|||
issue_content, user_llm, doc_meta
|
||||
)
|
||||
else:
|
||||
summary_content = f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}"
|
||||
summary_content = (
|
||||
f"Jira Issue {issue_identifier}: {issue_title}\n\n{issue_content}"
|
||||
)
|
||||
summary_embedding = embed_text(summary_content)
|
||||
|
||||
chunks = await create_document_chunks(issue_content)
|
||||
|
||||
document.title = f"{issue_identifier}: {issue_title}"
|
||||
document.content = summary_content
|
||||
document.content_hash = generate_content_hash(issue_content, search_space_id)
|
||||
document.content_hash = generate_content_hash(
|
||||
issue_content, search_space_id
|
||||
)
|
||||
document.embedding = summary_embedding
|
||||
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
|
@ -219,11 +240,15 @@ class JiraKBSyncService:
|
|||
|
||||
logger.info(
|
||||
"KB sync successful for document %s (%s: %s)",
|
||||
document_id, issue_identifier, issue_title,
|
||||
document_id,
|
||||
issue_identifier,
|
||||
issue_title,
|
||||
)
|
||||
return {"status": "success"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("KB sync failed for document %s: %s", document_id, e, exc_info=True)
|
||||
logger.error(
|
||||
"KB sync failed for document %s: %s", document_id, e, exc_info=True
|
||||
)
|
||||
await self.db_session.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class JiraToolMetadataService:
|
|||
|
||||
async def _check_account_health(self, connector: SearchSourceConnector) -> bool:
|
||||
"""Check if the Jira connector auth is still valid.
|
||||
|
||||
|
||||
Returns True if auth is expired/invalid, False if healthy.
|
||||
"""
|
||||
try:
|
||||
|
|
@ -98,9 +98,7 @@ class JiraToolMetadataService:
|
|||
await asyncio.to_thread(jira_client.get_myself)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Jira connector %s health check failed: %s", connector.id, e
|
||||
)
|
||||
logger.warning("Jira connector %s health check failed: %s", connector.id, e)
|
||||
try:
|
||||
connector.config = {**connector.config, "auth_expired": True}
|
||||
flag_modified(connector, "config")
|
||||
|
|
@ -116,7 +114,7 @@ class JiraToolMetadataService:
|
|||
|
||||
async def get_creation_context(self, search_space_id: int, user_id: str) -> dict:
|
||||
"""Return context needed to create a new Jira issue.
|
||||
|
||||
|
||||
Fetches all connected Jira accounts, and for the first healthy one
|
||||
fetches projects, issue types, and priorities.
|
||||
"""
|
||||
|
|
@ -165,7 +163,8 @@ class JiraToolMetadataService:
|
|||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to fetch Jira context for connector %s: %s",
|
||||
connector.id, e,
|
||||
connector.id,
|
||||
e,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -179,7 +178,7 @@ class JiraToolMetadataService:
|
|||
self, search_space_id: int, user_id: str, issue_ref: str
|
||||
) -> dict:
|
||||
"""Return context needed to update an indexed Jira issue.
|
||||
|
||||
|
||||
Resolves the issue from the KB, then fetches current details from the Jira API.
|
||||
"""
|
||||
document = await self._resolve_issue(search_space_id, user_id, issue_ref)
|
||||
|
|
@ -209,13 +208,15 @@ class JiraToolMetadataService:
|
|||
session=self._db_session, connector_id=connector.id
|
||||
)
|
||||
jira_client = await jira_history._get_jira_client()
|
||||
issue_data = await asyncio.to_thread(
|
||||
jira_client.get_issue, issue.issue_id
|
||||
)
|
||||
issue_data = await asyncio.to_thread(jira_client.get_issue, issue.issue_id)
|
||||
formatted = jira_client.format_issue(issue_data)
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "401" in error_str or "403" in error_str or "authentication" in error_str:
|
||||
if (
|
||||
"401" in error_str
|
||||
or "403" in error_str
|
||||
or "authentication" in error_str
|
||||
):
|
||||
return {
|
||||
"error": f"Failed to fetch Jira issue: {e!s}",
|
||||
"auth_expired": True,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ class LinearKBSyncService:
|
|||
if not indexable_content:
|
||||
indexable_content = f"Linear Issue {issue_identifier}: {issue_title}"
|
||||
|
||||
issue_content = f"# {issue_identifier}: {issue_title}\n\n{indexable_content}"
|
||||
issue_content = (
|
||||
f"# {issue_identifier}: {issue_title}\n\n{indexable_content}"
|
||||
)
|
||||
|
||||
content_hash = generate_content_hash(issue_content, search_space_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,11 @@ class LinearToolMetadataService:
|
|||
issue_api = await self._fetch_issue_context(linear_client, issue.id)
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "401" in error_str or "authentication" in error_str or "re-authenticate" in error_str:
|
||||
if (
|
||||
"401" in error_str
|
||||
or "authentication" in error_str
|
||||
or "re-authenticate" in error_str
|
||||
):
|
||||
return {
|
||||
"error": f"Failed to fetch Linear issue context: {e!s}",
|
||||
"auth_expired": True,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,10 @@ class NotionToolMetadataService:
|
|||
)
|
||||
db_connector = result.scalar_one_or_none()
|
||||
if db_connector and not db_connector.config.get("auth_expired"):
|
||||
db_connector.config = {**db_connector.config, "auth_expired": True}
|
||||
db_connector.config = {
|
||||
**db_connector.config,
|
||||
"auth_expired": True,
|
||||
}
|
||||
flag_modified(db_connector, "config")
|
||||
await self._db_session.commit()
|
||||
await self._db_session.refresh(db_connector)
|
||||
|
|
|
|||
|
|
@ -114,9 +114,7 @@ async def index_google_calendar_events(
|
|||
|
||||
# Build credentials based on connector type
|
||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||
connected_account_id = connector.config.get(
|
||||
"composio_connected_account_id"
|
||||
)
|
||||
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||
if not connected_account_id:
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
|
|
@ -396,10 +394,19 @@ async def index_google_calendar_events(
|
|||
session, legacy_hash
|
||||
)
|
||||
if existing_document:
|
||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
||||
if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
||||
existing_document.document_type = DocumentType.GOOGLE_CALENDAR_CONNECTOR
|
||||
logger.info(f"Migrated legacy Composio Calendar document: {event_id}")
|
||||
existing_document.unique_identifier_hash = (
|
||||
unique_identifier_hash
|
||||
)
|
||||
if (
|
||||
existing_document.document_type
|
||||
== DocumentType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
):
|
||||
existing_document.document_type = (
|
||||
DocumentType.GOOGLE_CALENDAR_CONNECTOR
|
||||
)
|
||||
logger.info(
|
||||
f"Migrated legacy Composio Calendar document: {event_id}"
|
||||
)
|
||||
|
||||
if existing_document:
|
||||
# Document exists - check if content has changed
|
||||
|
|
|
|||
|
|
@ -121,13 +121,13 @@ async def index_google_drive_files(
|
|||
# Build credentials based on connector type
|
||||
pre_built_credentials = None
|
||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||
connected_account_id = connector.config.get(
|
||||
"composio_connected_account_id"
|
||||
)
|
||||
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||
if not connected_account_id:
|
||||
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
||||
await task_logger.log_task_failure(
|
||||
log_entry, error_msg, "Missing Composio account",
|
||||
log_entry,
|
||||
error_msg,
|
||||
"Missing Composio account",
|
||||
{"error_type": "MissingComposioAccount"},
|
||||
)
|
||||
return 0, 0, error_msg
|
||||
|
|
@ -355,13 +355,13 @@ async def index_google_drive_single_file(
|
|||
|
||||
pre_built_credentials = None
|
||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||
connected_account_id = connector.config.get(
|
||||
"composio_connected_account_id"
|
||||
)
|
||||
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||
if not connected_account_id:
|
||||
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
||||
await task_logger.log_task_failure(
|
||||
log_entry, error_msg, "Missing Composio account",
|
||||
log_entry,
|
||||
error_msg,
|
||||
"Missing Composio account",
|
||||
{"error_type": "MissingComposioAccount"},
|
||||
)
|
||||
return 0, error_msg
|
||||
|
|
@ -611,7 +611,11 @@ async def _index_full_scan(
|
|||
|
||||
if not files_to_process and first_listing_error:
|
||||
error_lower = first_listing_error.lower()
|
||||
if "401" in first_listing_error or "invalid credentials" in error_lower or "authError" in first_listing_error:
|
||||
if (
|
||||
"401" in first_listing_error
|
||||
or "invalid credentials" in error_lower
|
||||
or "authError" in first_listing_error
|
||||
):
|
||||
raise Exception(
|
||||
f"Google Drive authentication failed. Please re-authenticate. "
|
||||
f"(Error: {first_listing_error})"
|
||||
|
|
@ -704,7 +708,11 @@ async def _index_with_delta_sync(
|
|||
if error:
|
||||
logger.error(f"Error fetching changes: {error}")
|
||||
error_lower = error.lower()
|
||||
if "401" in error or "invalid credentials" in error_lower or "authError" in error:
|
||||
if (
|
||||
"401" in error
|
||||
or "invalid credentials" in error_lower
|
||||
or "authError" in error
|
||||
):
|
||||
raise Exception(
|
||||
f"Google Drive authentication failed. Please re-authenticate. "
|
||||
f"(Error: {error})"
|
||||
|
|
@ -872,7 +880,10 @@ async def _create_pending_document_for_file(
|
|||
)
|
||||
if existing_document:
|
||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
||||
if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
|
||||
if (
|
||||
existing_document.document_type
|
||||
== DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
):
|
||||
existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE
|
||||
logger.info(f"Migrated legacy Composio document to native type: {file_id}")
|
||||
|
||||
|
|
@ -984,10 +995,12 @@ async def _check_rename_only_update(
|
|||
result = await session.execute(
|
||||
select(Document).where(
|
||||
Document.search_space_id == search_space_id,
|
||||
Document.document_type.in_([
|
||||
DocumentType.GOOGLE_DRIVE_FILE,
|
||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]),
|
||||
Document.document_type.in_(
|
||||
[
|
||||
DocumentType.GOOGLE_DRIVE_FILE,
|
||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]
|
||||
),
|
||||
cast(Document.document_metadata["google_drive_file_id"], String)
|
||||
== file_id,
|
||||
)
|
||||
|
|
@ -1000,7 +1013,10 @@ async def _check_rename_only_update(
|
|||
if existing_document:
|
||||
if existing_document.unique_identifier_hash != primary_hash:
|
||||
existing_document.unique_identifier_hash = primary_hash
|
||||
if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
|
||||
if (
|
||||
existing_document.document_type
|
||||
== DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
):
|
||||
existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE
|
||||
logger.info(f"Migrated legacy Composio Drive document: {file_id}")
|
||||
|
||||
|
|
@ -1232,10 +1248,12 @@ async def _remove_document(session: AsyncSession, file_id: str, search_space_id:
|
|||
result = await session.execute(
|
||||
select(Document).where(
|
||||
Document.search_space_id == search_space_id,
|
||||
Document.document_type.in_([
|
||||
DocumentType.GOOGLE_DRIVE_FILE,
|
||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]),
|
||||
Document.document_type.in_(
|
||||
[
|
||||
DocumentType.GOOGLE_DRIVE_FILE,
|
||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
]
|
||||
),
|
||||
cast(Document.document_metadata["google_drive_file_id"], String)
|
||||
== file_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -119,9 +119,7 @@ async def index_google_gmail_messages(
|
|||
|
||||
# Build credentials based on connector type
|
||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||
connected_account_id = connector.config.get(
|
||||
"composio_connected_account_id"
|
||||
)
|
||||
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||
if not connected_account_id:
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
|
|
@ -323,10 +321,19 @@ async def index_google_gmail_messages(
|
|||
session, legacy_hash
|
||||
)
|
||||
if existing_document:
|
||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
||||
if existing_document.document_type == DocumentType.COMPOSIO_GMAIL_CONNECTOR:
|
||||
existing_document.document_type = DocumentType.GOOGLE_GMAIL_CONNECTOR
|
||||
logger.info(f"Migrated legacy Composio Gmail document: {message_id}")
|
||||
existing_document.unique_identifier_hash = (
|
||||
unique_identifier_hash
|
||||
)
|
||||
if (
|
||||
existing_document.document_type
|
||||
== DocumentType.COMPOSIO_GMAIL_CONNECTOR
|
||||
):
|
||||
existing_document.document_type = (
|
||||
DocumentType.GOOGLE_GMAIL_CONNECTOR
|
||||
)
|
||||
logger.info(
|
||||
f"Migrated legacy Composio Gmail document: {message_id}"
|
||||
)
|
||||
|
||||
if existing_document:
|
||||
# Document exists - check if content has changed
|
||||
|
|
|
|||
|
|
@ -1270,9 +1270,16 @@ async def process_file_in_background(
|
|||
print("Error deleting temp file", e)
|
||||
pass
|
||||
|
||||
enable_summary = connector.get("enable_summary", True) if connector else True
|
||||
enable_summary = (
|
||||
connector.get("enable_summary", True) if connector else True
|
||||
)
|
||||
result = await add_received_file_document_using_unstructured(
|
||||
session, filename, docs, search_space_id, user_id, connector,
|
||||
session,
|
||||
filename,
|
||||
docs,
|
||||
search_space_id,
|
||||
user_id,
|
||||
connector,
|
||||
enable_summary=enable_summary,
|
||||
)
|
||||
|
||||
|
|
@ -1414,7 +1421,9 @@ async def process_file_in_background(
|
|||
# Extract text content from the markdown documents
|
||||
markdown_content = doc.text
|
||||
|
||||
enable_summary = connector.get("enable_summary", True) if connector else True
|
||||
enable_summary = (
|
||||
connector.get("enable_summary", True) if connector else True
|
||||
)
|
||||
doc_result = await add_received_file_document_using_llamacloud(
|
||||
session,
|
||||
filename,
|
||||
|
|
@ -1569,7 +1578,9 @@ async def process_file_in_background(
|
|||
session, notification, stage="chunking"
|
||||
)
|
||||
|
||||
enable_summary = connector.get("enable_summary", True) if connector else True
|
||||
enable_summary = (
|
||||
connector.get("enable_summary", True) if connector else True
|
||||
)
|
||||
doc_result = await add_received_file_document_using_docling(
|
||||
session,
|
||||
filename,
|
||||
|
|
|
|||
|
|
@ -156,9 +156,7 @@ async def committed_google_data(async_engine):
|
|||
session.add(user)
|
||||
await session.flush()
|
||||
|
||||
space = SearchSpace(
|
||||
name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id
|
||||
)
|
||||
space = SearchSpace(name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id)
|
||||
session.add(space)
|
||||
await session.flush()
|
||||
space_id = space.id
|
||||
|
|
@ -215,7 +213,9 @@ async def committed_google_data(async_engine):
|
|||
def patched_session_factory(async_engine, monkeypatch):
|
||||
"""Replace ``async_session_maker`` in connector_service with one bound to the test engine."""
|
||||
test_maker = async_sessionmaker(async_engine, expire_on_commit=False)
|
||||
monkeypatch.setattr("app.services.connector_service.async_session_maker", test_maker)
|
||||
monkeypatch.setattr(
|
||||
"app.services.connector_service.async_session_maker", test_maker
|
||||
)
|
||||
return test_maker
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
|||
|
||||
from app.db import SearchSourceConnectorType
|
||||
|
||||
from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector
|
||||
from .conftest import (
|
||||
cleanup_space,
|
||||
make_session_factory,
|
||||
mock_task_logger,
|
||||
seed_connector,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
|
@ -52,8 +57,10 @@ async def native_calendar(async_engine):
|
|||
async_engine,
|
||||
connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
|
||||
config={
|
||||
"token": "fake", "refresh_token": "fake",
|
||||
"client_id": "fake", "client_secret": "fake",
|
||||
"token": "fake",
|
||||
"refresh_token": "fake",
|
||||
"client_id": "fake",
|
||||
"client_secret": "fake",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
},
|
||||
name_prefix="cal-native",
|
||||
|
|
@ -66,10 +73,16 @@ async def native_calendar(async_engine):
|
|||
@patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_composio_calendar_uses_composio_credentials(
|
||||
mock_build_creds, mock_cal_cls, mock_tl_cls, async_engine, composio_calendar,
|
||||
mock_build_creds,
|
||||
mock_cal_cls,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
composio_calendar,
|
||||
):
|
||||
"""Calendar indexer calls build_composio_credentials for a Composio connector."""
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import (
|
||||
index_google_calendar_events,
|
||||
)
|
||||
|
||||
data = composio_calendar
|
||||
mock_creds = MagicMock(name="composio-creds")
|
||||
|
|
@ -77,14 +90,18 @@ async def test_composio_calendar_uses_composio_credentials(
|
|||
mock_tl_cls.return_value = mock_task_logger()
|
||||
|
||||
mock_cal_instance = MagicMock()
|
||||
mock_cal_instance.get_all_primary_calendar_events = AsyncMock(return_value=([], None))
|
||||
mock_cal_instance.get_all_primary_calendar_events = AsyncMock(
|
||||
return_value=([], None)
|
||||
)
|
||||
mock_cal_cls.return_value = mock_cal_instance
|
||||
|
||||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
await index_google_calendar_events(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
mock_build_creds.assert_called_once_with(_COMPOSIO_ACCOUNT_ID)
|
||||
|
|
@ -96,10 +113,15 @@ async def test_composio_calendar_uses_composio_credentials(
|
|||
@patch(f"{_INDEXER_MODULE}.TaskLoggingService")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_composio_calendar_without_account_id_returns_error(
|
||||
mock_build_creds, mock_tl_cls, async_engine, composio_calendar_no_id,
|
||||
mock_build_creds,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
composio_calendar_no_id,
|
||||
):
|
||||
"""Calendar indexer returns error when Composio connector lacks connected_account_id."""
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import (
|
||||
index_google_calendar_events,
|
||||
)
|
||||
|
||||
data = composio_calendar_no_id
|
||||
mock_tl_cls.return_value = mock_task_logger()
|
||||
|
|
@ -107,8 +129,10 @@ async def test_composio_calendar_without_account_id_returns_error(
|
|||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
count, _skipped, error = await index_google_calendar_events(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
assert count == 0
|
||||
|
|
@ -121,23 +145,33 @@ async def test_composio_calendar_without_account_id_returns_error(
|
|||
@patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_native_calendar_does_not_use_composio_credentials(
|
||||
mock_build_creds, mock_cal_cls, mock_tl_cls, async_engine, native_calendar,
|
||||
mock_build_creds,
|
||||
mock_cal_cls,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
native_calendar,
|
||||
):
|
||||
"""Calendar indexer does NOT call build_composio_credentials for a native connector."""
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import index_google_calendar_events
|
||||
from app.tasks.connector_indexers.google_calendar_indexer import (
|
||||
index_google_calendar_events,
|
||||
)
|
||||
|
||||
data = native_calendar
|
||||
mock_tl_cls.return_value = mock_task_logger()
|
||||
|
||||
mock_cal_instance = MagicMock()
|
||||
mock_cal_instance.get_all_primary_calendar_events = AsyncMock(return_value=([], None))
|
||||
mock_cal_instance.get_all_primary_calendar_events = AsyncMock(
|
||||
return_value=([], None)
|
||||
)
|
||||
mock_cal_cls.return_value = mock_cal_instance
|
||||
|
||||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
await index_google_calendar_events(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
mock_build_creds.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
|||
|
||||
from app.db import SearchSourceConnectorType
|
||||
|
||||
from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector
|
||||
from .conftest import (
|
||||
cleanup_space,
|
||||
make_session_factory,
|
||||
mock_task_logger,
|
||||
seed_connector,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
|
@ -129,7 +134,9 @@ async def test_composio_connector_without_account_id_returns_error(
|
|||
|
||||
assert count == 0
|
||||
assert error is not None
|
||||
assert "composio_connected_account_id" in error.lower() or "composio" in error.lower()
|
||||
assert (
|
||||
"composio_connected_account_id" in error.lower() or "composio" in error.lower()
|
||||
)
|
||||
mock_build_creds.assert_not_called()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
|||
|
||||
from app.db import SearchSourceConnectorType
|
||||
|
||||
from .conftest import cleanup_space, make_session_factory, mock_task_logger, seed_connector
|
||||
from .conftest import (
|
||||
cleanup_space,
|
||||
make_session_factory,
|
||||
mock_task_logger,
|
||||
seed_connector,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
|
@ -52,8 +57,10 @@ async def native_gmail(async_engine):
|
|||
async_engine,
|
||||
connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||
config={
|
||||
"token": "fake", "refresh_token": "fake",
|
||||
"client_id": "fake", "client_secret": "fake",
|
||||
"token": "fake",
|
||||
"refresh_token": "fake",
|
||||
"client_id": "fake",
|
||||
"client_secret": "fake",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
},
|
||||
name_prefix="gmail-native",
|
||||
|
|
@ -66,10 +73,16 @@ async def native_gmail(async_engine):
|
|||
@patch(f"{_INDEXER_MODULE}.GoogleGmailConnector")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_composio_gmail_uses_composio_credentials(
|
||||
mock_build_creds, mock_gmail_cls, mock_tl_cls, async_engine, composio_gmail,
|
||||
mock_build_creds,
|
||||
mock_gmail_cls,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
composio_gmail,
|
||||
):
|
||||
"""Gmail indexer calls build_composio_credentials for a Composio connector."""
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import (
|
||||
index_google_gmail_messages,
|
||||
)
|
||||
|
||||
data = composio_gmail
|
||||
mock_creds = MagicMock(name="composio-creds")
|
||||
|
|
@ -83,8 +96,10 @@ async def test_composio_gmail_uses_composio_credentials(
|
|||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
await index_google_gmail_messages(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
mock_build_creds.assert_called_once_with(_COMPOSIO_ACCOUNT_ID)
|
||||
|
|
@ -96,10 +111,15 @@ async def test_composio_gmail_uses_composio_credentials(
|
|||
@patch(f"{_INDEXER_MODULE}.TaskLoggingService")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_composio_gmail_without_account_id_returns_error(
|
||||
mock_build_creds, mock_tl_cls, async_engine, composio_gmail_no_id,
|
||||
mock_build_creds,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
composio_gmail_no_id,
|
||||
):
|
||||
"""Gmail indexer returns error when Composio connector lacks connected_account_id."""
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import (
|
||||
index_google_gmail_messages,
|
||||
)
|
||||
|
||||
data = composio_gmail_no_id
|
||||
mock_tl_cls.return_value = mock_task_logger()
|
||||
|
|
@ -107,8 +127,10 @@ async def test_composio_gmail_without_account_id_returns_error(
|
|||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
count, _skipped, error = await index_google_gmail_messages(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
assert count == 0
|
||||
|
|
@ -121,10 +143,16 @@ async def test_composio_gmail_without_account_id_returns_error(
|
|||
@patch(f"{_INDEXER_MODULE}.GoogleGmailConnector")
|
||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||
async def test_native_gmail_does_not_use_composio_credentials(
|
||||
mock_build_creds, mock_gmail_cls, mock_tl_cls, async_engine, native_gmail,
|
||||
mock_build_creds,
|
||||
mock_gmail_cls,
|
||||
mock_tl_cls,
|
||||
async_engine,
|
||||
native_gmail,
|
||||
):
|
||||
"""Gmail indexer does NOT call build_composio_credentials for a native connector."""
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import index_google_gmail_messages
|
||||
from app.tasks.connector_indexers.google_gmail_indexer import (
|
||||
index_google_gmail_messages,
|
||||
)
|
||||
|
||||
data = native_gmail
|
||||
mock_tl_cls.return_value = mock_task_logger()
|
||||
|
|
@ -136,8 +164,10 @@ async def test_native_gmail_does_not_use_composio_credentials(
|
|||
maker = make_session_factory(async_engine)
|
||||
async with maker() as session:
|
||||
await index_google_gmail_messages(
|
||||
session=session, connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
||||
session=session,
|
||||
connector_id=data["connector_id"],
|
||||
search_space_id=data["search_space_id"],
|
||||
user_id=data["user_id"],
|
||||
)
|
||||
|
||||
mock_build_creds.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -39,9 +39,7 @@ async def test_list_of_types_returns_both_matching_doc_types(
|
|||
assert "FILE" not in returned_types
|
||||
|
||||
|
||||
async def test_single_string_type_returns_only_that_type(
|
||||
db_session, seed_google_docs
|
||||
):
|
||||
async def test_single_string_type_returns_only_that_type(db_session, seed_google_docs):
|
||||
"""Searching with a single string type returns only documents of that exact type."""
|
||||
space_id = seed_google_docs["search_space"].id
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ async def test_gmail_accepts_valid_composio_credentials(mock_build):
|
|||
mock_build.return_value = mock_service
|
||||
|
||||
connector = GoogleGmailConnector(
|
||||
creds, session=MagicMock(), user_id="test-user",
|
||||
creds,
|
||||
session=MagicMock(),
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
profile, error = await connector.get_user_profile()
|
||||
|
|
@ -76,7 +78,9 @@ async def test_gmail_accepts_valid_composio_credentials(mock_build):
|
|||
|
||||
@patch("app.connectors.google_gmail_connector.Request")
|
||||
@patch("app.connectors.google_gmail_connector.build")
|
||||
async def test_gmail_refreshes_expired_composio_credentials(mock_build, mock_request_cls):
|
||||
async def test_gmail_refreshes_expired_composio_credentials(
|
||||
mock_build, mock_request_cls
|
||||
):
|
||||
"""GoogleGmailConnector handles expired Composio credentials via refresh_handler
|
||||
without attempting DB persistence."""
|
||||
from app.connectors.google_gmail_connector import GoogleGmailConnector
|
||||
|
|
@ -95,7 +99,9 @@ async def test_gmail_refreshes_expired_composio_credentials(mock_build, mock_req
|
|||
|
||||
mock_session = AsyncMock()
|
||||
connector = GoogleGmailConnector(
|
||||
creds, session=mock_session, user_id="test-user",
|
||||
creds,
|
||||
session=mock_session,
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
profile, error = await connector.get_user_profile()
|
||||
|
|
@ -128,7 +134,9 @@ async def test_calendar_accepts_valid_composio_credentials(mock_build):
|
|||
mock_build.return_value = mock_service
|
||||
|
||||
connector = GoogleCalendarConnector(
|
||||
creds, session=MagicMock(), user_id="test-user",
|
||||
creds,
|
||||
session=MagicMock(),
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
calendars, error = await connector.get_calendars()
|
||||
|
|
@ -141,7 +149,9 @@ async def test_calendar_accepts_valid_composio_credentials(mock_build):
|
|||
|
||||
@patch("app.connectors.google_calendar_connector.Request")
|
||||
@patch("app.connectors.google_calendar_connector.build")
|
||||
async def test_calendar_refreshes_expired_composio_credentials(mock_build, mock_request_cls):
|
||||
async def test_calendar_refreshes_expired_composio_credentials(
|
||||
mock_build, mock_request_cls
|
||||
):
|
||||
"""GoogleCalendarConnector handles expired Composio credentials via refresh_handler
|
||||
without attempting DB persistence."""
|
||||
from app.connectors.google_calendar_connector import GoogleCalendarConnector
|
||||
|
|
@ -157,7 +167,9 @@ async def test_calendar_refreshes_expired_composio_credentials(mock_build, mock_
|
|||
|
||||
mock_session = AsyncMock()
|
||||
connector = GoogleCalendarConnector(
|
||||
creds, session=mock_session, user_id="test-user",
|
||||
creds,
|
||||
session=mock_session,
|
||||
user_id="test-user",
|
||||
)
|
||||
|
||||
calendars, error = await connector.get_calendars()
|
||||
|
|
@ -191,7 +203,9 @@ async def test_drive_client_uses_prebuilt_composio_credentials(mock_build):
|
|||
mock_build.return_value = mock_service
|
||||
|
||||
client = GoogleDriveClient(
|
||||
session=MagicMock(), connector_id=999, credentials=creds,
|
||||
session=MagicMock(),
|
||||
connector_id=999,
|
||||
credentials=creds,
|
||||
)
|
||||
|
||||
files, next_token, error = await client.list_files()
|
||||
|
|
@ -218,7 +232,9 @@ async def test_drive_client_prebuilt_creds_skip_db_loading(mock_build, mock_get_
|
|||
mock_build.return_value = mock_service
|
||||
|
||||
client = GoogleDriveClient(
|
||||
session=MagicMock(), connector_id=999, credentials=creds,
|
||||
session=MagicMock(),
|
||||
connector_id=999,
|
||||
credentials=creds,
|
||||
)
|
||||
|
||||
await client.list_files()
|
||||
|
|
|
|||
|
|
@ -20,8 +20,14 @@ def test_drive_indexer_accepts_both_native_and_composio():
|
|||
ACCEPTED_DRIVE_CONNECTOR_TYPES,
|
||||
)
|
||||
|
||||
assert SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
||||
assert (
|
||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR
|
||||
in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
||||
)
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
def test_gmail_indexer_accepts_both_native_and_composio():
|
||||
|
|
@ -30,8 +36,14 @@ def test_gmail_indexer_accepts_both_native_and_composio():
|
|||
ACCEPTED_GMAIL_CONNECTOR_TYPES,
|
||||
)
|
||||
|
||||
assert SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
||||
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
||||
assert (
|
||||
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR
|
||||
in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
||||
)
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
def test_calendar_indexer_accepts_both_native_and_composio():
|
||||
|
|
@ -40,14 +52,29 @@ def test_calendar_indexer_accepts_both_native_and_composio():
|
|||
ACCEPTED_CALENDAR_CONNECTOR_TYPES,
|
||||
)
|
||||
|
||||
assert SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
||||
assert (
|
||||
SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR
|
||||
in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
||||
)
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
def test_composio_connector_types_set_covers_all_google_services():
|
||||
"""COMPOSIO_GOOGLE_CONNECTOR_TYPES should contain all three Composio Google types."""
|
||||
from app.utils.google_credentials import COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
|
||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||
in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
)
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR
|
||||
in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
)
|
||||
assert (
|
||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR
|
||||
in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||
)
|
||||
|
|
|
|||
|
|
@ -213,11 +213,7 @@ export function LocalLoginForm() {
|
|||
disabled={isLoggingIn}
|
||||
className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoggingIn ? (
|
||||
<Spinner size="sm" className="text-white" />
|
||||
) : (
|
||||
t("sign_in")
|
||||
)}
|
||||
{isLoggingIn ? <Spinner size="sm" className="text-white" /> : t("sign_in")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ export async function GET(
|
|||
connectorId: searchParams.get("connectorId"),
|
||||
});
|
||||
|
||||
const redirectUrl = new URL(
|
||||
`/dashboard/${search_space_id}/new-chat`,
|
||||
request.url
|
||||
);
|
||||
const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url);
|
||||
|
||||
const response = NextResponse.redirect(redirectUrl, { status: 302 });
|
||||
response.cookies.set(OAUTH_RESULT_COOKIE, result, {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
|
|
@ -16,12 +17,11 @@ import {
|
|||
Trash2,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||
import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { toast } from "sonner";
|
||||
import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
|
||||
import { membersAtom } from "@/atoms/members/members-query.atoms";
|
||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
|
|
@ -35,14 +35,9 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
|
|
@ -51,7 +46,12 @@ import {
|
|||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -37,21 +37,26 @@ import { Thread } from "@/components/assistant-ui/thread";
|
|||
import { MobileEditorPanel } from "@/components/editor-panel/editor-panel";
|
||||
import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel";
|
||||
import { MobileReportPanel } from "@/components/report-panel/report-panel";
|
||||
import {
|
||||
CreateConfluencePageToolUI,
|
||||
DeleteConfluencePageToolUI,
|
||||
UpdateConfluencePageToolUI,
|
||||
} from "@/components/tool-ui/confluence";
|
||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
|
||||
import {
|
||||
CreateCalendarEventToolUI,
|
||||
UpdateCalendarEventToolUI,
|
||||
DeleteCalendarEventToolUI,
|
||||
} from "@/components/tool-ui/google-calendar";
|
||||
import {
|
||||
CreateGmailDraftToolUI,
|
||||
SendGmailEmailToolUI,
|
||||
TrashGmailEmailToolUI,
|
||||
UpdateGmailDraftToolUI,
|
||||
} from "@/components/tool-ui/gmail";
|
||||
import {
|
||||
CreateCalendarEventToolUI,
|
||||
DeleteCalendarEventToolUI,
|
||||
UpdateCalendarEventToolUI,
|
||||
} from "@/components/tool-ui/google-calendar";
|
||||
import {
|
||||
CreateGoogleDriveFileToolUI,
|
||||
DeleteGoogleDriveFileToolUI,
|
||||
|
|
@ -61,11 +66,6 @@ import {
|
|||
DeleteJiraIssueToolUI,
|
||||
UpdateJiraIssueToolUI,
|
||||
} from "@/components/tool-ui/jira";
|
||||
import {
|
||||
CreateConfluencePageToolUI,
|
||||
DeleteConfluencePageToolUI,
|
||||
UpdateConfluencePageToolUI,
|
||||
} from "@/components/tool-ui/confluence";
|
||||
import {
|
||||
CreateLinearIssueToolUI,
|
||||
DeleteLinearIssueToolUI,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
UserPlus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -44,6 +43,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import NextError from "next/error";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function GlobalError({
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ interface HitlEditPanelState {
|
|||
content: string;
|
||||
toolName: string;
|
||||
extraFields?: ExtraField[];
|
||||
onSave: ((title: string, content: string, extraFieldValues?: Record<string, string>) => void) | null;
|
||||
onSave:
|
||||
| ((title: string, content: string, extraFieldValues?: Record<string, string>) => void)
|
||||
| null;
|
||||
onClose: (() => void) | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -214,11 +214,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
if (!searchSpaceId) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
modal={false}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
|
||||
{showTrigger && (
|
||||
<TooltipIconButton
|
||||
data-joyride="connector-icon"
|
||||
|
|
@ -354,11 +350,12 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
onBack={handleBackFromEdit}
|
||||
onQuickIndex={(() => {
|
||||
const cfg = connectorConfig || editingConnector.config;
|
||||
const isDrive = editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
const isDrive =
|
||||
editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" ||
|
||||
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
|
||||
const hasDriveItems = isDrive
|
||||
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
||||
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
||||
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
||||
: true;
|
||||
if (!hasDriveItems) return undefined;
|
||||
return () => {
|
||||
|
|
@ -376,37 +373,37 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
onNameChange={setConnectorName}
|
||||
/>
|
||||
) : indexingConfig ? (
|
||||
<IndexingConfigurationView
|
||||
config={indexingConfig}
|
||||
connector={
|
||||
indexingConnector
|
||||
? {
|
||||
...indexingConnector,
|
||||
config: indexingConnectorConfig || indexingConnector.config,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
periodicEnabled={periodicEnabled}
|
||||
frequencyMinutes={frequencyMinutes}
|
||||
enableSummary={enableSummary}
|
||||
isStartingIndexing={isStartingIndexing}
|
||||
isFromOAuth={isFromOAuth}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
onPeriodicEnabledChange={setPeriodicEnabled}
|
||||
onFrequencyChange={setFrequencyMinutes}
|
||||
onEnableSummaryChange={setEnableSummary}
|
||||
onConfigChange={setIndexingConnectorConfig}
|
||||
onStartIndexing={() => {
|
||||
if (indexingConfig.connectorId) {
|
||||
startIndexing(indexingConfig.connectorId);
|
||||
<IndexingConfigurationView
|
||||
config={indexingConfig}
|
||||
connector={
|
||||
indexingConnector
|
||||
? {
|
||||
...indexingConnector,
|
||||
config: indexingConnectorConfig || indexingConnector.config,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
handleStartIndexing(() => refreshConnectors());
|
||||
}}
|
||||
onSkip={handleSkipIndexing}
|
||||
/>
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
periodicEnabled={periodicEnabled}
|
||||
frequencyMinutes={frequencyMinutes}
|
||||
enableSummary={enableSummary}
|
||||
isStartingIndexing={isStartingIndexing}
|
||||
isFromOAuth={isFromOAuth}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
onPeriodicEnabledChange={setPeriodicEnabled}
|
||||
onFrequencyChange={setFrequencyMinutes}
|
||||
onEnableSummaryChange={setEnableSummary}
|
||||
onConfigChange={setIndexingConnectorConfig}
|
||||
onStartIndexing={() => {
|
||||
if (indexingConfig.connectorId) {
|
||||
startIndexing(indexingConfig.connectorId);
|
||||
}
|
||||
handleStartIndexing(() => refreshConnectors());
|
||||
}}
|
||||
onSkip={handleSkipIndexing}
|
||||
/>
|
||||
) : (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Info } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useId, useRef, useState } from "react";
|
||||
|
|
@ -26,6 +25,7 @@ import {
|
|||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
|
|||
|
|
@ -82,10 +82,7 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
|
|||
return <File className={`${className} text-gray-500`} />;
|
||||
}
|
||||
|
||||
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
|
||||
connector,
|
||||
onConfigChange,
|
||||
}) => {
|
||||
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
|
||||
const isIndexable = connector.config?.is_indexable as boolean;
|
||||
|
||||
const existingFolders =
|
||||
|
|
@ -236,47 +233,48 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your Google Drive authentication has expired. Please re-authenticate using the button below.
|
||||
</p>
|
||||
)}
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your Google Drive authentication has expired. Please re-authenticate using the button
|
||||
below.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isEditMode ? (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
|
||||
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
|
||||
>
|
||||
Change Selection
|
||||
{isFolderTreeOpen ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
{isEditMode ? (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
|
||||
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
|
||||
>
|
||||
Change Selection
|
||||
{isFolderTreeOpen ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
{isFolderTreeOpen && (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{isFolderTreeOpen && (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ComposioDriveFolderTree
|
||||
connectorId={connector.id}
|
||||
selectedFolders={selectedFolders}
|
||||
onSelectFolders={handleSelectFolders}
|
||||
selectedFiles={selectedFiles}
|
||||
onSelectFiles={handleSelectFiles}
|
||||
onAuthError={handleAuthError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Indexing Options */}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { KeyRound, Server } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
|
||||
export interface ElasticsearchConfigProps extends ConnectorConfigProps {
|
||||
|
|
|
|||
|
|
@ -231,26 +231,25 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={openPicker}
|
||||
disabled={pickerLoading || isAuthExpired}
|
||||
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
|
||||
>
|
||||
{pickerLoading && <Spinner size="xs" className="mr-1.5" />}
|
||||
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={openPicker}
|
||||
disabled={pickerLoading || isAuthExpired}
|
||||
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
|
||||
>
|
||||
{pickerLoading && <Spinner size="xs" className="mr-1.5" />}
|
||||
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
||||
</Button>
|
||||
|
||||
{pickerError && !isAuthExpired && (
|
||||
<p className="text-xs text-destructive">{pickerError}</p>
|
||||
)}
|
||||
{pickerError && !isAuthExpired && <p className="text-xs text-destructive">{pickerError}</p>}
|
||||
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your Google Drive authentication has expired. Please re-authenticate using the button below.
|
||||
</p>
|
||||
)}
|
||||
{isAuthExpired && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||
Your Google Drive authentication has expired. Please re-authenticate using the button
|
||||
below.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Indexing Options */}
|
||||
|
|
|
|||
|
|
@ -220,10 +220,8 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Quick Index Button - hidden when auth is expired */}
|
||||
{connector.is_indexable &&
|
||||
onQuickIndex &&
|
||||
!isAuthExpired && (
|
||||
{/* Quick Index Button - hidden when auth is expired */}
|
||||
{connector.is_indexable && onQuickIndex && !isAuthExpired && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
|
@ -401,31 +399,31 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
{isAuthExpired && reauthEndpoint ? (
|
||||
<Button
|
||||
onClick={handleReauth}
|
||||
disabled={reauthing || isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2 bg-amber-600 hover:bg-amber-700 text-white"
|
||||
>
|
||||
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
|
||||
Re-authenticate
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={isSaving || isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Saving
|
||||
</>
|
||||
) : (
|
||||
"Save Changes"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{isAuthExpired && reauthEndpoint ? (
|
||||
<Button
|
||||
onClick={handleReauth}
|
||||
disabled={reauthing || isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2 bg-amber-600 hover:bg-amber-700 text-white"
|
||||
>
|
||||
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
|
||||
Re-authenticate
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={isSaving || isDisconnecting}
|
||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
Saving
|
||||
</>
|
||||
) : (
|
||||
"Save Changes"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -261,35 +261,28 @@ export const useConnectorDialog = () => {
|
|||
| (typeof COMPOSIO_CONNECTORS)[number]
|
||||
| undefined;
|
||||
|
||||
if (result.connectorId) {
|
||||
const connectorId = parseInt(result.connectorId, 10);
|
||||
newConnector = fetchResult.data.find(
|
||||
(c: SearchSourceConnector) => c.id === connectorId
|
||||
);
|
||||
if (newConnector) {
|
||||
const connectorType = newConnector.connector_type;
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find(
|
||||
(c) => c.connectorType === connectorType
|
||||
) ||
|
||||
COMPOSIO_CONNECTORS.find(
|
||||
(c) => c.connectorType === connectorType
|
||||
);
|
||||
if (result.connectorId) {
|
||||
const connectorId = parseInt(result.connectorId, 10);
|
||||
newConnector = fetchResult.data.find((c: SearchSourceConnector) => c.id === connectorId);
|
||||
if (newConnector) {
|
||||
const connectorType = newConnector.connector_type;
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!newConnector && result.connector) {
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
|
||||
if (oauthConnector) {
|
||||
const oauthType = oauthConnector.connectorType;
|
||||
newConnector = fetchResult.data.find(
|
||||
(c: SearchSourceConnector) =>
|
||||
c.connector_type === oauthType
|
||||
);
|
||||
if (!newConnector && result.connector) {
|
||||
oauthConnector =
|
||||
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
|
||||
if (oauthConnector) {
|
||||
const oauthType = oauthConnector.connectorType;
|
||||
newConnector = fetchResult.data.find(
|
||||
(c: SearchSourceConnector) => c.connector_type === oauthType
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newConnector && oauthConnector) {
|
||||
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
||||
|
|
@ -599,17 +592,17 @@ export const useConnectorDialog = () => {
|
|||
: `${connectorTitle} connected and syncing started!`;
|
||||
toast.success(successMessage);
|
||||
|
||||
setIsOpen(false);
|
||||
setIsOpen(false);
|
||||
|
||||
setIndexingConfig(null);
|
||||
setIndexingConnector(null);
|
||||
setIndexingConnectorConfig(null);
|
||||
setIndexingConfig(null);
|
||||
setIndexingConnector(null);
|
||||
setIndexingConnectorConfig(null);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
|
||||
await refetchAllConnectors();
|
||||
await refetchAllConnectors();
|
||||
} else {
|
||||
// Non-indexable connector
|
||||
// For Circleback, transition to edit view to show webhook URL
|
||||
|
|
@ -631,11 +624,11 @@ export const useConnectorDialog = () => {
|
|||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
|
||||
toast.success(`${connectorTitle} connected successfully!`, {
|
||||
description: "Configure the webhook URL in your Circleback settings.",
|
||||
});
|
||||
toast.success(`${connectorTitle} connected successfully!`, {
|
||||
description: "Configure the webhook URL in your Circleback settings.",
|
||||
});
|
||||
|
||||
await refetchAllConnectors();
|
||||
await refetchAllConnectors();
|
||||
} else {
|
||||
// Other non-indexable connectors - just show success message and close
|
||||
const successMessage =
|
||||
|
|
@ -644,13 +637,13 @@ export const useConnectorDialog = () => {
|
|||
: `${connectorTitle} connected successfully!`;
|
||||
toast.success(successMessage);
|
||||
|
||||
await refetchAllConnectors();
|
||||
await refetchAllConnectors();
|
||||
|
||||
setIsOpen(false);
|
||||
setIsOpen(false);
|
||||
|
||||
setIndexingConfig(null);
|
||||
setIndexingConnector(null);
|
||||
setIndexingConnectorConfig(null);
|
||||
setIndexingConfig(null);
|
||||
setIndexingConnector(null);
|
||||
setIndexingConnectorConfig(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -870,12 +863,12 @@ export const useConnectorDialog = () => {
|
|||
);
|
||||
}
|
||||
|
||||
toast.success(`${indexingConfig.connectorTitle} indexing started`);
|
||||
toast.success(`${indexingConfig.connectorTitle} indexing started`);
|
||||
|
||||
setIsOpen(false);
|
||||
setIsFromOAuth(false);
|
||||
setIsOpen(false);
|
||||
setIsFromOAuth(false);
|
||||
|
||||
refreshConnectors();
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
|
|
@ -927,21 +920,21 @@ export const useConnectorDialog = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Track if we came from accounts list view so handleBackFromEdit can restore it
|
||||
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
|
||||
setCameFromAccountsList(viewingAccountsType);
|
||||
} else {
|
||||
setCameFromAccountsList(null);
|
||||
}
|
||||
setViewingAccountsType(null);
|
||||
// Track if we came from accounts list view so handleBackFromEdit can restore it
|
||||
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
|
||||
setCameFromAccountsList(viewingAccountsType);
|
||||
} else {
|
||||
setCameFromAccountsList(null);
|
||||
}
|
||||
setViewingAccountsType(null);
|
||||
|
||||
// Track if we came from MCP list view so handleBackFromEdit can restore it
|
||||
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
|
||||
setCameFromMCPList(true);
|
||||
} else {
|
||||
setCameFromMCPList(false);
|
||||
}
|
||||
setViewingMCPList(false);
|
||||
// Track if we came from MCP list view so handleBackFromEdit can restore it
|
||||
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
|
||||
setCameFromMCPList(true);
|
||||
} else {
|
||||
setCameFromMCPList(false);
|
||||
}
|
||||
setViewingMCPList(false);
|
||||
|
||||
// Track index with date range opened event
|
||||
if (connector.is_indexable) {
|
||||
|
|
@ -952,15 +945,15 @@ export const useConnectorDialog = () => {
|
|||
);
|
||||
}
|
||||
|
||||
setEditingConnector(connector);
|
||||
setConnectorName(connector.name);
|
||||
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
|
||||
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
|
||||
setEnableSummary(connector.enable_summary ?? false);
|
||||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
},
|
||||
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
||||
setEditingConnector(connector);
|
||||
setConnectorName(connector.name);
|
||||
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
|
||||
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
|
||||
setEnableSummary(connector.enable_summary ?? false);
|
||||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
},
|
||||
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
||||
);
|
||||
|
||||
// Handle saving connector changes
|
||||
|
|
@ -1139,35 +1132,35 @@ export const useConnectorDialog = () => {
|
|||
: indexingDescription,
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
setIsOpen(false);
|
||||
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving connector:", error);
|
||||
toast.error("Failed to save connector changes");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
editingConnector,
|
||||
searchSpaceId,
|
||||
isSaving,
|
||||
startDate,
|
||||
endDate,
|
||||
indexConnector,
|
||||
updateConnector,
|
||||
periodicEnabled,
|
||||
frequencyMinutes,
|
||||
enableSummary,
|
||||
getFrequencyLabel,
|
||||
connectorConfig,
|
||||
connectorName,
|
||||
setIsOpen,
|
||||
]
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving connector:", error);
|
||||
toast.error("Failed to save connector changes");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
editingConnector,
|
||||
searchSpaceId,
|
||||
isSaving,
|
||||
startDate,
|
||||
endDate,
|
||||
indexConnector,
|
||||
updateConnector,
|
||||
periodicEnabled,
|
||||
frequencyMinutes,
|
||||
enableSummary,
|
||||
getFrequencyLabel,
|
||||
connectorConfig,
|
||||
connectorName,
|
||||
setIsOpen,
|
||||
]
|
||||
);
|
||||
|
||||
// Handle disconnecting connector
|
||||
|
|
@ -1194,19 +1187,19 @@ export const useConnectorDialog = () => {
|
|||
: `${editingConnector.name} disconnected successfully`
|
||||
);
|
||||
|
||||
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
|
||||
setViewingMCPList(true);
|
||||
setEditingConnector(null);
|
||||
setConnectorName(null);
|
||||
setConnectorConfig(null);
|
||||
} else {
|
||||
setEditingConnector(null);
|
||||
setConnectorName(null);
|
||||
setConnectorConfig(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
|
||||
setViewingMCPList(true);
|
||||
setEditingConnector(null);
|
||||
setConnectorName(null);
|
||||
setConnectorConfig(null);
|
||||
} else {
|
||||
setEditingConnector(null);
|
||||
setConnectorName(null);
|
||||
setConnectorConfig(null);
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
refreshConnectors();
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
|
|
@ -1312,13 +1305,13 @@ export const useConnectorDialog = () => {
|
|||
setEditingConnector(null);
|
||||
setConnectorName(null);
|
||||
setConnectorConfig(null);
|
||||
setConnectingConnectorType(null);
|
||||
setViewingAccountsType(null);
|
||||
setViewingMCPList(false);
|
||||
setCameFromAccountsList(null);
|
||||
setCameFromMCPList(false);
|
||||
setConnectCameFromMCPList(false);
|
||||
setStartDate(undefined);
|
||||
setConnectingConnectorType(null);
|
||||
setViewingAccountsType(null);
|
||||
setViewingMCPList(false);
|
||||
setCameFromAccountsList(null);
|
||||
setCameFromMCPList(false);
|
||||
setConnectCameFromMCPList(false);
|
||||
setStartDate(undefined);
|
||||
setEndDate(undefined);
|
||||
setPeriodicEnabled(false);
|
||||
setFrequencyMinutes("1440");
|
||||
|
|
|
|||
|
|
@ -203,8 +203,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{typeConnectors.map((connector) => {
|
||||
const isIndexing = indexingConnectorIds.has(connector.id);
|
||||
const isAuthExpired =
|
||||
!!reauthEndpoint && connector.config?.auth_expired === true;
|
||||
const isAuthExpired = !!reauthEndpoint && connector.config?.auth_expired === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -252,7 +251,9 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
onClick={() => handleReauth(connector.id)}
|
||||
disabled={reauthingId === connector.id}
|
||||
>
|
||||
<RefreshCw className={cn("size-3.5", reauthingId === connector.id && "animate-spin")} />
|
||||
<RefreshCw
|
||||
className={cn("size-3.5", reauthingId === connector.id && "animate-spin")}
|
||||
/>
|
||||
Re-authenticate
|
||||
</Button>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
|
|||
|
||||
const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress");
|
||||
const allCompleted =
|
||||
steps.length > 0 && !isThreadRunning && steps.every((s) => getEffectiveStatus(s) === "completed");
|
||||
steps.length > 0 &&
|
||||
!isThreadRunning &&
|
||||
steps.every((s) => getEffectiveStatus(s) === "completed");
|
||||
const isProcessing = isThreadRunning && !allCompleted;
|
||||
|
||||
// Auto-collapse when all tasks are completed
|
||||
|
|
@ -127,7 +129,7 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
|
|||
effectiveStatus === "pending" && "text-muted-foreground/60"
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
{step.title}
|
||||
</div>
|
||||
|
||||
{/* Step items (sub-content) */}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { CONNECTOR_ICON_TO_TYPES, CONNECTOR_TOOL_ICON_PATHS, getToolIcon } from "@/contracts/enums/toolIcons";
|
||||
import {
|
||||
CONNECTOR_ICON_TO_TYPES,
|
||||
CONNECTOR_TOOL_ICON_PATHS,
|
||||
getToolIcon,
|
||||
} from "@/contracts/enums/toolIcons";
|
||||
import type { Document } from "@/contracts/types/document.types";
|
||||
import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
||||
import { useCommentsElectric } from "@/hooks/use-comments-electric";
|
||||
|
|
@ -735,71 +739,75 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
</span>
|
||||
</div>
|
||||
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
|
||||
{groupedTools.filter((g) => !g.connectorIcon).map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
||||
{group.label}
|
||||
{groupedTools
|
||||
.filter((g) => !g.connectorIcon)
|
||||
.map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
||||
{group.label}
|
||||
</div>
|
||||
{group.tools.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
const ToolIcon = getToolIcon(tool.name);
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
||||
{formatToolName(tool.name)}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!isDisabled}
|
||||
onCheckedChange={() => toggleTool(tool.name)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{group.tools.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
const ToolIcon = getToolIcon(tool.name);
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
||||
{formatToolName(tool.name)}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!isDisabled}
|
||||
onCheckedChange={() => toggleTool(tool.name)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
{groupedTools.some((g) => g.connectorIcon) && (
|
||||
<div>
|
||||
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
||||
Connector Actions
|
||||
</div>
|
||||
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
|
||||
const iconKey = group.connectorIcon ?? "";
|
||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||
const toolNames = group.tools.map((t) => t.name);
|
||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||
return (
|
||||
<div
|
||||
key={group.label}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
{iconInfo ? (
|
||||
<Image
|
||||
src={iconInfo.src}
|
||||
alt={iconInfo.alt}
|
||||
width={18}
|
||||
height={18}
|
||||
className="size-[18px] shrink-0 select-none pointer-events-none"
|
||||
draggable={false}
|
||||
{groupedTools
|
||||
.filter((g) => g.connectorIcon)
|
||||
.map((group) => {
|
||||
const iconKey = group.connectorIcon ?? "";
|
||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||
const toolNames = group.tools.map((t) => t.name);
|
||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||
return (
|
||||
<div
|
||||
key={group.label}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
||||
>
|
||||
{iconInfo ? (
|
||||
<Image
|
||||
src={iconInfo.src}
|
||||
alt={iconInfo.alt}
|
||||
width={18}
|
||||
height={18}
|
||||
className="size-[18px] shrink-0 select-none pointer-events-none"
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<Wrench className="size-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
||||
{group.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!allDisabled}
|
||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<Wrench className="size-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1 min-w-0 text-sm font-medium truncate">
|
||||
{group.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!allDisabled}
|
||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!filteredTools?.length && (
|
||||
|
|
@ -857,82 +865,87 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
|||
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
}}
|
||||
>
|
||||
{groupedTools.filter((g) => !g.connectorIcon).map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
|
||||
{group.label}
|
||||
{groupedTools
|
||||
.filter((g) => !g.connectorIcon)
|
||||
.map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
|
||||
{group.label}
|
||||
</div>
|
||||
{group.tools.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
const ToolIcon = getToolIcon(tool.name);
|
||||
const row = (
|
||||
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
|
||||
<ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
|
||||
{formatToolName(tool.name)}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!isDisabled}
|
||||
onCheckedChange={() => toggleTool(tool.name)}
|
||||
className="shrink-0 scale-[0.6] sm:scale-75"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tooltip key={tool.name}>
|
||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-64 text-xs">
|
||||
{tool.description}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{group.tools.map((tool) => {
|
||||
const isDisabled = disabledTools.includes(tool.name);
|
||||
const ToolIcon = getToolIcon(tool.name);
|
||||
const row = (
|
||||
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
|
||||
<ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
|
||||
{formatToolName(tool.name)}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!isDisabled}
|
||||
onCheckedChange={() => toggleTool(tool.name)}
|
||||
className="shrink-0 scale-[0.6] sm:scale-75"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tooltip key={tool.name}>
|
||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-64 text-xs">
|
||||
{tool.description}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
{groupedTools.some((g) => g.connectorIcon) && (
|
||||
<div>
|
||||
<div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
|
||||
Connector Actions
|
||||
</div>
|
||||
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
|
||||
const iconKey = group.connectorIcon ?? "";
|
||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||
const toolNames = group.tools.map((t) => t.name);
|
||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
|
||||
const row = (
|
||||
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
|
||||
{iconInfo ? (
|
||||
<Image
|
||||
src={iconInfo.src}
|
||||
alt={iconInfo.alt}
|
||||
width={16}
|
||||
height={16}
|
||||
className="size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
|
||||
draggable={false}
|
||||
{groupedTools
|
||||
.filter((g) => g.connectorIcon)
|
||||
.map((group) => {
|
||||
const iconKey = group.connectorIcon ?? "";
|
||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||
const toolNames = group.tools.map((t) => t.name);
|
||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
|
||||
const row = (
|
||||
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
|
||||
{iconInfo ? (
|
||||
<Image
|
||||
src={iconInfo.src}
|
||||
alt={iconInfo.alt}
|
||||
width={16}
|
||||
height={16}
|
||||
className="size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<Wrench className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
|
||||
{group.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!allDisabled}
|
||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
||||
className="shrink-0 scale-[0.6] sm:scale-75"
|
||||
/>
|
||||
) : (
|
||||
<Wrench className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
|
||||
{group.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={!allDisabled}
|
||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
||||
className="shrink-0 scale-[0.6] sm:scale-75"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tooltip key={group.label}>
|
||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-72 text-xs">
|
||||
{groupDef?.tooltip ?? group.tools.map((t) => t.description).join(" · ")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tooltip key={group.label}>
|
||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-72 text-xs">
|
||||
{groupDef?.tooltip ??
|
||||
group.tools.map((t) => t.description).join(" · ")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!filteredTools?.length && (
|
||||
|
|
|
|||
|
|
@ -78,14 +78,21 @@ export function ComposioDriveFolderTree({
|
|||
}: ComposioDriveFolderTreeProps) {
|
||||
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
|
||||
|
||||
const { data: rootData, isLoading: isLoadingRoot, error: rootError } = useComposioDriveFolders({
|
||||
const {
|
||||
data: rootData,
|
||||
isLoading: isLoadingRoot,
|
||||
error: rootError,
|
||||
} = useComposioDriveFolders({
|
||||
connectorId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (rootError && onAuthError) {
|
||||
const msg = rootError instanceof Error ? rootError.message : String(rootError);
|
||||
if (msg.toLowerCase().includes("authentication expired") || msg.toLowerCase().includes("re-authenticate")) {
|
||||
if (
|
||||
msg.toLowerCase().includes("authentication expired") ||
|
||||
msg.toLowerCase().includes("re-authenticate")
|
||||
) {
|
||||
onAuthError(msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -363,19 +370,21 @@ export function ComposioDriveFolderTree({
|
|||
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
|
||||
</div>
|
||||
|
||||
{!isLoadingRoot && rootError && (
|
||||
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
|
||||
{(rootError instanceof Error ? rootError.message : String(rootError)).includes("authentication expired")
|
||||
? "Google Drive authentication has expired. Please re-authenticate above."
|
||||
: "Failed to load Google Drive contents."}
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingRoot && rootError && (
|
||||
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
|
||||
{(rootError instanceof Error ? rootError.message : String(rootError)).includes(
|
||||
"authentication expired"
|
||||
)
|
||||
? "Google Drive authentication has expired. Please re-authenticate above."
|
||||
: "Failed to load Google Drive contents."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
|
||||
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
|
||||
No files or folders found in your Google Drive
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
|
||||
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
|
||||
No files or folders found in your Google Drive
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { TagInput, type Tag as TagType } from "emblor";
|
||||
import { format } from "date-fns";
|
||||
import { TagInput, type Tag as TagType } from "emblor";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { CalendarIcon, XIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
closeHitlEditPanelAtom,
|
||||
hitlEditPanelAtom,
|
||||
} from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -58,14 +55,9 @@ function EmailsTagField({
|
|||
onChangeRef.current(tagsToEmailString(tags));
|
||||
}, [tags]);
|
||||
|
||||
const handleSetTags = useCallback(
|
||||
(newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
|
||||
setTags((prev) =>
|
||||
typeof newTags === "function" ? newTags(prev) : newTags
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleSetTags = useCallback((newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
|
||||
setTags((prev) => (typeof newTags === "function" ? newTags(prev) : newTags));
|
||||
}, []);
|
||||
|
||||
const handleAddTag = useCallback(
|
||||
(text: string) => {
|
||||
|
|
@ -265,7 +257,10 @@ export function HitlEditPanelContent({
|
|||
<div className="flex flex-col gap-3 px-4 py-3 border-b">
|
||||
{extraFields.map((field) => (
|
||||
<div key={field.key} className="flex flex-col gap-1.5">
|
||||
<Label htmlFor={`extra-field-${field.key}`} className="text-xs font-medium text-muted-foreground">
|
||||
<Label
|
||||
htmlFor={`extra-field-${field.key}`}
|
||||
className="text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{field.label}
|
||||
</Label>
|
||||
{field.type === "emails" ? (
|
||||
|
|
@ -360,9 +355,7 @@ function MobileHitlEditDrawer() {
|
|||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
<DrawerTitle className="sr-only">
|
||||
Edit {panelState.toolName}
|
||||
</DrawerTitle>
|
||||
<DrawerTitle className="sr-only">Edit {panelState.toolName}</DrawerTitle>
|
||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||
<HitlEditPanelContent
|
||||
title={panelState.title}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTheme } from "next-themes";
|
||||
|
|
@ -22,6 +21,10 @@ import {
|
|||
userSettingsDialogAtom,
|
||||
} from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
|
||||
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
||||
import { TeamDialog } from "@/components/settings/team-dialog";
|
||||
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -42,7 +45,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
||||
import { useInbox } from "@/hooks/use-inbox";
|
||||
|
|
@ -54,10 +57,6 @@ import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-pers
|
|||
import { cleanupElectric } from "@/lib/electric/client";
|
||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
|
||||
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
||||
import { TeamDialog } from "@/components/settings/team-dialog";
|
||||
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
|
||||
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
||||
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
||||
import { LayoutShell } from "../ui/shell";
|
||||
|
|
@ -822,11 +821,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
disabled={isDeletingChat}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
||||
>
|
||||
{isDeletingChat ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
tCommon("delete")
|
||||
)}
|
||||
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,13 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
|||
} else if (effectiveTab === "report" && !reportOpen) {
|
||||
effectiveTab = editorOpen ? "editor" : "sources";
|
||||
} else if (effectiveTab === "sources" && !documentsOpen) {
|
||||
effectiveTab = hitlEditOpen ? "hitl-edit" : editorOpen ? "editor" : reportOpen ? "report" : "sources";
|
||||
effectiveTab = hitlEditOpen
|
||||
? "hitl-edit"
|
||||
: editorOpen
|
||||
? "editor"
|
||||
: reportOpen
|
||||
? "report"
|
||||
: "sources";
|
||||
}
|
||||
|
||||
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
||||
|
|
@ -160,14 +166,14 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
|||
)}
|
||||
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
|
||||
<div className="h-full flex flex-col">
|
||||
<HitlEditPanelContent
|
||||
title={hitlEditState.title}
|
||||
content={hitlEditState.content}
|
||||
toolName={hitlEditState.toolName}
|
||||
extraFields={hitlEditState.extraFields}
|
||||
onSave={hitlEditState.onSave}
|
||||
onClose={closeHitlEdit}
|
||||
/>
|
||||
<HitlEditPanelContent
|
||||
title={hitlEditState.title}
|
||||
content={hitlEditState.content}
|
||||
toolName={hitlEditState.toolName}
|
||||
extraFields={hitlEditState.extraFields}
|
||||
onSave={hitlEditState.onSave}
|
||||
onClose={closeHitlEdit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -36,7 +37,6 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -36,7 +37,6 @@ import {
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { useLongPress } from "@/hooks/use-long-press";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
createImageGenConfigMutationAtom,
|
||||
deleteImageGenConfigMutationAtom,
|
||||
|
|
@ -38,6 +37,7 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
|
|
@ -69,12 +69,12 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import {
|
||||
getImageGenModelsByProvider,
|
||||
IMAGE_GEN_PROVIDERS,
|
||||
} from "@/contracts/enums/image-gen-providers";
|
||||
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { getProviderIcon } from "@/lib/provider-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||
import {
|
||||
createNewLLMConfigMutationAtom,
|
||||
|
|
@ -36,6 +35,7 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { useAtom } from "jotai";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content";
|
||||
import { teamDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content";
|
||||
|
||||
interface TeamDialogProps {
|
||||
searchSpaceId: number;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import { useAtom } from "jotai";
|
||||
import { KeyRound, User } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent";
|
||||
import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent";
|
||||
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { SettingsDialog } from "@/components/settings/settings-dialog";
|
||||
|
||||
export function UserSettingsDialog() {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -11,11 +15,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface ConfluenceAccount {
|
||||
id: number;
|
||||
|
|
@ -108,9 +108,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
|||
);
|
||||
}
|
||||
|
||||
function isInsufficientPermissionsResult(
|
||||
result: unknown,
|
||||
): result is InsufficientPermissionsResult {
|
||||
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
|
|
@ -161,7 +159,7 @@ function ApprovalCard({
|
|||
space_id: selectedSpaceId || null,
|
||||
};
|
||||
},
|
||||
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits],
|
||||
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits]
|
||||
);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
|
|
@ -177,7 +175,17 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -202,15 +210,16 @@ function ApprovalCard({
|
|||
: "Create Confluence Page"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Creating page with your changes" : "Creating page"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating page with your changes" : "Creating page"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "Page created with your changes" : "Page created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Page creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -225,8 +234,8 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.title ?? (args.title ?? ""),
|
||||
content: pendingEdits?.content ?? (args.content ?? ""),
|
||||
title: pendingEdits?.title ?? args.title ?? "",
|
||||
content: pendingEdits?.content ?? args.content ?? "",
|
||||
toolName: "Confluence Page",
|
||||
onSave: (newTitle, newContent) => {
|
||||
setIsPanelOpen(false);
|
||||
|
|
@ -290,10 +299,7 @@ function ApprovalCard({
|
|||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Space <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select
|
||||
value={selectedSpaceId}
|
||||
onValueChange={setSelectedSpaceId}
|
||||
>
|
||||
<Select value={selectedSpaceId} onValueChange={setSelectedSpaceId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a space" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -379,9 +385,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
All Confluence accounts expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">All Confluence accounts expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -391,9 +395,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
);
|
||||
}
|
||||
|
||||
function InsufficientPermissionsCard({
|
||||
result,
|
||||
}: { result: InsufficientPermissionsResult }) {
|
||||
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
@ -474,7 +476,8 @@ export const CreateConfluencePageToolUI = makeAssistantToolUI<
|
|||
}
|
||||
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result))
|
||||
return <InsufficientPermissionsCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
|
||||
return <SuccessCard result={result as SuccessResult} />;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { CornerDownLeftIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface InterruptResult {
|
||||
|
|
@ -132,9 +132,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
|||
);
|
||||
}
|
||||
|
||||
function isInsufficientPermissionsResult(
|
||||
result: unknown,
|
||||
): result is InsufficientPermissionsResult {
|
||||
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
|
|
@ -174,7 +172,15 @@ function ApprovalCard({
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, onDecision, interruptData, page?.page_id, context?.account?.id, deleteFromKb]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
page?.page_id,
|
||||
context?.account?.id,
|
||||
deleteFromKb,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -203,9 +209,7 @@ function ApprovalCard({
|
|||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Page deletion was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -238,9 +242,7 @@ function ApprovalCard({
|
|||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
|
||||
<div className="font-medium">{page.page_title}</div>
|
||||
{page.space_id && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Space: {page.space_id}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -279,11 +281,7 @@ function ApprovalCard({
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
@ -309,9 +307,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Confluence authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -321,9 +317,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
);
|
||||
}
|
||||
|
||||
function InsufficientPermissionsCard({
|
||||
result,
|
||||
}: { result: InsufficientPermissionsResult }) {
|
||||
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
@ -357,9 +351,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Page not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -437,7 +429,8 @@ export const DeleteConfluencePageToolUI = makeAssistantToolUI<
|
|||
|
||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result))
|
||||
return <InsufficientPermissionsCard result={result} />;
|
||||
if (isWarningResult(result)) return <WarningCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
|||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface InterruptResult {
|
||||
__interrupt__: true;
|
||||
|
|
@ -116,9 +116,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
|||
);
|
||||
}
|
||||
|
||||
function isInsufficientPermissionsResult(
|
||||
result: unknown,
|
||||
): result is InsufficientPermissionsResult {
|
||||
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
|
|
@ -169,8 +167,7 @@ function ApprovalCard({
|
|||
const canEdit = allowedDecisions.includes("edit");
|
||||
|
||||
const hasProposedChanges =
|
||||
actionArgs.new_title || args.new_title ||
|
||||
actionArgs.new_content || args.new_content;
|
||||
actionArgs.new_title || args.new_title || actionArgs.new_content || args.new_content;
|
||||
|
||||
const buildFinalArgs = useCallback(() => {
|
||||
return {
|
||||
|
|
@ -196,7 +193,16 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
hasPanelEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -221,15 +227,16 @@ function ApprovalCard({
|
|||
: "Update Confluence Page"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={hasPanelEdits ? "Updating page with your changes" : "Updating page"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={hasPanelEdits ? "Updating page with your changes" : "Updating page"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{hasPanelEdits ? "Page updated with your changes" : "Page updated"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Page update was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -293,7 +300,8 @@ function ApprovalCard({
|
|||
className="max-h-[5rem] overflow-hidden text-xs text-muted-foreground"
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
|
|
@ -306,9 +314,7 @@ function ApprovalCard({
|
|||
</div>
|
||||
)}
|
||||
{page.space_id && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Space: {page.space_id}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -322,14 +328,18 @@ function ApprovalCard({
|
|||
{/* Content preview — proposed changes */}
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(hasProposedChanges || hasPanelEdits) ? (
|
||||
{hasProposedChanges || hasPanelEdits ? (
|
||||
<>
|
||||
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}
|
||||
{String(
|
||||
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content)) && (
|
||||
{(hasPanelEdits
|
||||
? editedArgs.content
|
||||
: (actionArgs.new_content ?? args.new_content)) && (
|
||||
<div
|
||||
className="max-h-[7rem] overflow-hidden text-sm"
|
||||
style={{
|
||||
|
|
@ -338,7 +348,11 @@ function ApprovalCard({
|
|||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
markdown={String(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content))}
|
||||
markdown={String(
|
||||
hasPanelEdits
|
||||
? editedArgs.content
|
||||
: (actionArgs.new_content ?? args.new_content)
|
||||
)}
|
||||
readOnly
|
||||
preset="readonly"
|
||||
editorVariant="none"
|
||||
|
|
@ -393,9 +407,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Confluence authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -405,9 +417,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
);
|
||||
}
|
||||
|
||||
function InsufficientPermissionsCard({
|
||||
result,
|
||||
}: { result: InsufficientPermissionsResult }) {
|
||||
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
@ -441,9 +451,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Page not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -509,7 +517,8 @@ export const UpdateConfluencePageToolUI = makeAssistantToolUI<
|
|||
|
||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||
if (isAuthErrorResult(result)) return <AuthErrorCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result)) return <InsufficientPermissionsCard result={result} />;
|
||||
if (isInsufficientPermissionsResult(result))
|
||||
return <InsufficientPermissionsCard result={result} />;
|
||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||
|
||||
return <SuccessCard result={result as SuccessResult} />;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CornerDownLeftIcon,
|
||||
Pen,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -16,11 +16,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GmailAccount {
|
||||
|
|
@ -132,7 +127,11 @@ function ApprovalCard({
|
|||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{
|
||||
subject: string; body: string; to: string; cc: string; bcc: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
to: string;
|
||||
cc: string;
|
||||
bcc: string;
|
||||
} | null>(null);
|
||||
|
||||
const accounts = interruptData.context?.accounts ?? [];
|
||||
|
|
@ -175,7 +174,18 @@ function ApprovalCard({
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
args,
|
||||
selectedAccountId,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -200,16 +210,17 @@ function ApprovalCard({
|
|||
? "Gmail Draft Approved"
|
||||
: "Create Gmail Draft"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Creating draft with your changes" : "Creating draft"} size="sm" />
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating draft with your changes" : "Creating draft"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "Draft created with your changes" : "Draft created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Draft creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Draft creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -225,13 +236,28 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
const extraFields: ExtraField[] = [
|
||||
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
|
||||
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
|
||||
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
|
||||
{
|
||||
key: "to",
|
||||
label: "To",
|
||||
type: "emails",
|
||||
value: pendingEdits?.to ?? args.to ?? "",
|
||||
},
|
||||
{
|
||||
key: "cc",
|
||||
label: "CC",
|
||||
type: "emails",
|
||||
value: pendingEdits?.cc ?? args.cc ?? "",
|
||||
},
|
||||
{
|
||||
key: "bcc",
|
||||
label: "BCC",
|
||||
type: "emails",
|
||||
value: pendingEdits?.bcc ?? args.bcc ?? "",
|
||||
},
|
||||
];
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.subject ?? (args.subject ?? ""),
|
||||
content: pendingEdits?.body ?? (args.body ?? ""),
|
||||
title: pendingEdits?.subject ?? args.subject ?? "",
|
||||
content: pendingEdits?.body ?? args.body ?? "",
|
||||
toolName: "Gmail Draft",
|
||||
extraFields,
|
||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||
|
|
@ -322,7 +348,9 @@ function ApprovalCard({
|
|||
|
||||
<div className="px-5 pt-1">
|
||||
{(pendingEdits?.subject ?? args.subject) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{pendingEdits?.subject ?? args.subject}
|
||||
</p>
|
||||
)}
|
||||
{(pendingEdits?.body ?? args.body) != null && (
|
||||
<div
|
||||
|
|
@ -398,9 +426,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Gmail authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CornerDownLeftIcon,
|
||||
MailIcon,
|
||||
Pen,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -17,11 +16,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GmailAccount {
|
||||
|
|
@ -132,7 +126,11 @@ function ApprovalCard({
|
|||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{
|
||||
subject: string; body: string; to: string; cc: string; bcc: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
to: string;
|
||||
cc: string;
|
||||
bcc: string;
|
||||
} | null>(null);
|
||||
|
||||
const accounts = interruptData.context?.accounts ?? [];
|
||||
|
|
@ -175,7 +173,18 @@ function ApprovalCard({
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
args,
|
||||
selectedAccountId,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -200,16 +209,17 @@ function ApprovalCard({
|
|||
? "Email Sending Approved"
|
||||
: "Send Email"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Sending email with your changes" : "Sending email"} size="sm" />
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Sending email with your changes" : "Sending email"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "Email sent with your changes" : "Email sent"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Email sending was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Email sending was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -225,13 +235,28 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
const extraFields: ExtraField[] = [
|
||||
{ key: "to", label: "To", type: "emails", value: pendingEdits?.to ?? args.to ?? "" },
|
||||
{ key: "cc", label: "CC", type: "emails", value: pendingEdits?.cc ?? args.cc ?? "" },
|
||||
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
|
||||
{
|
||||
key: "to",
|
||||
label: "To",
|
||||
type: "emails",
|
||||
value: pendingEdits?.to ?? args.to ?? "",
|
||||
},
|
||||
{
|
||||
key: "cc",
|
||||
label: "CC",
|
||||
type: "emails",
|
||||
value: pendingEdits?.cc ?? args.cc ?? "",
|
||||
},
|
||||
{
|
||||
key: "bcc",
|
||||
label: "BCC",
|
||||
type: "emails",
|
||||
value: pendingEdits?.bcc ?? args.bcc ?? "",
|
||||
},
|
||||
];
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.subject ?? (args.subject ?? ""),
|
||||
content: pendingEdits?.body ?? (args.body ?? ""),
|
||||
title: pendingEdits?.subject ?? args.subject ?? "",
|
||||
content: pendingEdits?.body ?? args.body ?? "",
|
||||
toolName: "Send Email",
|
||||
extraFields,
|
||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||
|
|
@ -264,32 +289,32 @@ function ApprovalCard({
|
|||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||
) : (
|
||||
accounts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Gmail Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={String(account.id)}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredAccounts.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{a.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Gmail Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={String(account.id)}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredAccounts.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{a.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -320,7 +345,9 @@ function ApprovalCard({
|
|||
|
||||
<div className="px-5 pt-1">
|
||||
{(pendingEdits?.subject ?? args.subject) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{pendingEdits?.subject ?? args.subject}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{pendingEdits?.subject ?? args.subject}
|
||||
</p>
|
||||
)}
|
||||
{(pendingEdits?.body ?? args.body) != null && (
|
||||
<div
|
||||
|
|
@ -396,9 +423,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Gmail authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CalendarIcon,
|
||||
CornerDownLeftIcon,
|
||||
MailIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { CalendarIcon, CornerDownLeftIcon, MailIcon, UserIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GmailAccount {
|
||||
|
|
@ -192,14 +187,12 @@ function ApprovalCard({
|
|||
? "Email Trash Approved"
|
||||
: "Trash Email"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Trashing email" size="sm" />
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Email trash was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Email trash was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -280,11 +273,7 @@ function ApprovalCard({
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
@ -324,9 +313,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Gmail authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CornerDownLeftIcon,
|
||||
MailIcon,
|
||||
Pen,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GmailAccount {
|
||||
|
|
@ -127,15 +121,12 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
|||
);
|
||||
}
|
||||
|
||||
function isInsufficientPermissionsResult(
|
||||
result: unknown,
|
||||
): result is InsufficientPermissionsResult {
|
||||
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||
return (
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
"status" in result &&
|
||||
(result as InsufficientPermissionsResult).status ===
|
||||
"insufficient_permissions"
|
||||
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -177,17 +168,11 @@ function ApprovalCard({
|
|||
const existingBody = context?.existing_body;
|
||||
|
||||
const reviewConfig = interruptData.review_configs?.[0];
|
||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? [
|
||||
"approve",
|
||||
"reject",
|
||||
];
|
||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||
const canEdit = allowedDecisions.includes("edit");
|
||||
|
||||
const currentSubject =
|
||||
pendingEdits?.subject ??
|
||||
args.subject ??
|
||||
email?.subject ??
|
||||
args.draft_subject_or_id;
|
||||
pendingEdits?.subject ?? args.subject ?? email?.subject ?? args.draft_subject_or_id;
|
||||
const currentBody = pendingEdits?.body ?? args.body;
|
||||
const currentTo = pendingEdits?.to ?? args.to ?? "";
|
||||
const currentCc = pendingEdits?.cc ?? args.cc ?? "";
|
||||
|
|
@ -259,23 +244,15 @@ function ApprovalCard({
|
|||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={
|
||||
pendingEdits
|
||||
? "Updating draft with your changes"
|
||||
: "Updating draft"
|
||||
}
|
||||
text={pendingEdits ? "Updating draft with your changes" : "Updating draft"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits
|
||||
? "Draft updated with your changes"
|
||||
: "Draft updated"}
|
||||
{pendingEdits ? "Draft updated with your changes" : "Draft updated"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Draft update was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Draft update was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -310,16 +287,12 @@ function ApprovalCard({
|
|||
value: currentBcc,
|
||||
},
|
||||
];
|
||||
openHitlEditPanel({
|
||||
title: currentSubject,
|
||||
content: editableBody,
|
||||
toolName: "Gmail Draft",
|
||||
openHitlEditPanel({
|
||||
title: currentSubject,
|
||||
content: editableBody,
|
||||
toolName: "Gmail Draft",
|
||||
extraFields,
|
||||
onSave: (
|
||||
newTitle,
|
||||
newContent,
|
||||
extraFieldValues,
|
||||
) => {
|
||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||
setIsPanelOpen(false);
|
||||
const extras = extraFieldValues ?? {};
|
||||
setPendingEdits({
|
||||
|
|
@ -346,16 +319,12 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-4 select-none">
|
||||
{context.error ? (
|
||||
<p className="text-sm text-destructive">
|
||||
{context.error}
|
||||
</p>
|
||||
<p className="text-sm text-destructive">{context.error}</p>
|
||||
) : (
|
||||
<>
|
||||
{account && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Gmail Account
|
||||
</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">Gmail Account</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||
{account.name}
|
||||
</div>
|
||||
|
|
@ -364,15 +333,11 @@ function ApprovalCard({
|
|||
|
||||
{email && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Draft to Update
|
||||
</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">Draft to Update</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<MailIcon className="size-3 shrink-0 text-muted-foreground" />
|
||||
<span className="font-medium">
|
||||
{email.subject}
|
||||
</span>
|
||||
<span className="font-medium">{email.subject}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -408,18 +373,14 @@ function ApprovalCard({
|
|||
|
||||
<div className="px-5 pt-1">
|
||||
{currentSubject != null && (
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{currentSubject}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-foreground">{currentSubject}</p>
|
||||
)}
|
||||
{editableBody ? (
|
||||
<div
|
||||
{editableBody ? (
|
||||
<div
|
||||
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
|
|
@ -477,9 +438,7 @@ function ErrorCard({ result }: { result: ErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Failed to update Gmail draft
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Failed to update Gmail draft</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -493,9 +452,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Gmail authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -505,9 +462,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
);
|
||||
}
|
||||
|
||||
function InsufficientPermissionsCard({
|
||||
result,
|
||||
}: { result: InsufficientPermissionsResult }) {
|
||||
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
|
|
@ -577,7 +532,7 @@ export const UpdateGmailDraftToolUI = makeAssistantToolUI<
|
|||
window.dispatchEvent(
|
||||
new CustomEvent("hitl-decision", {
|
||||
detail: { decisions: [decision] },
|
||||
}),
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
ClockIcon,
|
||||
MapPinIcon,
|
||||
UsersIcon,
|
||||
GlobeIcon,
|
||||
CornerDownLeftIcon,
|
||||
Pen,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { ClockIcon, CornerDownLeftIcon, GlobeIcon, MapPinIcon, Pen, UsersIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -19,11 +16,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface GoogleCalendarAccount {
|
||||
id: number;
|
||||
|
|
@ -160,8 +153,12 @@ function ApprovalCard({
|
|||
const [wasEdited, setWasEdited] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{
|
||||
summary: string; description: string; start_datetime: string;
|
||||
end_datetime: string; location: string; attendees: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
start_datetime: string;
|
||||
end_datetime: string;
|
||||
location: string;
|
||||
attendees: string;
|
||||
} | null>(null);
|
||||
|
||||
const accounts = interruptData.context?.accounts ?? [];
|
||||
|
|
@ -236,7 +233,19 @@ function ApprovalCard({
|
|||
args: finalArgs,
|
||||
},
|
||||
});
|
||||
}, [phase, isPanelOpen, canApprove, allowedDecisions, setProcessing, onDecision, interruptData, args, selectedAccountId, selectedCalendarId, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
args,
|
||||
selectedAccountId,
|
||||
selectedCalendarId,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -250,7 +259,10 @@ function ApprovalCard({
|
|||
|
||||
const attendeesList = (args.attendees as string[]) ?? [];
|
||||
const displayAttendees = pendingEdits?.attendees
|
||||
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
|
||||
? pendingEdits.attendees
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean)
|
||||
: attendeesList;
|
||||
|
||||
return (
|
||||
|
|
@ -266,16 +278,17 @@ function ApprovalCard({
|
|||
? "Calendar Event Approved"
|
||||
: "Create Calendar Event"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={wasEdited ? "Creating event with your changes" : "Creating event"} size="sm" />
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={wasEdited ? "Creating event with your changes" : "Creating event"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{wasEdited ? "Event created with your changes" : "Event created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Event creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Event creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -291,14 +304,34 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
const extraFields: ExtraField[] = [
|
||||
{ key: "start_datetime", label: "Start", type: "datetime-local", value: pendingEdits?.start_datetime ?? args.start_datetime ?? "" },
|
||||
{ key: "end_datetime", label: "End", type: "datetime-local", value: pendingEdits?.end_datetime ?? args.end_datetime ?? "" },
|
||||
{ key: "location", label: "Location", type: "text", value: pendingEdits?.location ?? args.location ?? "" },
|
||||
{ key: "attendees", label: "Attendees", type: "emails", value: pendingEdits?.attendees ?? attendeesList.join(", ") },
|
||||
{
|
||||
key: "start_datetime",
|
||||
label: "Start",
|
||||
type: "datetime-local",
|
||||
value: pendingEdits?.start_datetime ?? args.start_datetime ?? "",
|
||||
},
|
||||
{
|
||||
key: "end_datetime",
|
||||
label: "End",
|
||||
type: "datetime-local",
|
||||
value: pendingEdits?.end_datetime ?? args.end_datetime ?? "",
|
||||
},
|
||||
{
|
||||
key: "location",
|
||||
label: "Location",
|
||||
type: "text",
|
||||
value: pendingEdits?.location ?? args.location ?? "",
|
||||
},
|
||||
{
|
||||
key: "attendees",
|
||||
label: "Attendees",
|
||||
type: "emails",
|
||||
value: pendingEdits?.attendees ?? attendeesList.join(", "),
|
||||
},
|
||||
];
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.summary ?? (args.summary ?? ""),
|
||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
||||
title: pendingEdits?.summary ?? args.summary ?? "",
|
||||
content: pendingEdits?.description ?? args.description ?? "",
|
||||
toolName: "Calendar Event",
|
||||
extraFields,
|
||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||
|
|
@ -307,10 +340,16 @@ function ApprovalCard({
|
|||
setPendingEdits({
|
||||
summary: newTitle,
|
||||
description: newContent,
|
||||
start_datetime: extras.start_datetime ?? pendingEdits?.start_datetime ?? args.start_datetime ?? "",
|
||||
end_datetime: extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
|
||||
start_datetime:
|
||||
extras.start_datetime ??
|
||||
pendingEdits?.start_datetime ??
|
||||
args.start_datetime ??
|
||||
"",
|
||||
end_datetime:
|
||||
extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_datetime ?? "",
|
||||
location: extras.location ?? pendingEdits?.location ?? args.location ?? "",
|
||||
attendees: extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
|
||||
attendees:
|
||||
extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
|
||||
});
|
||||
},
|
||||
onClose: () => setIsPanelOpen(false),
|
||||
|
|
@ -372,7 +411,8 @@ function ApprovalCard({
|
|||
<SelectContent>
|
||||
{calendars.map((cal) => (
|
||||
<SelectItem key={cal.id} value={cal.id}>
|
||||
{cal.summary}{cal.primary ? " (primary)" : ""}
|
||||
{cal.summary}
|
||||
{cal.primary ? " (primary)" : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -399,16 +439,26 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3 pb-3 space-y-2">
|
||||
{(pendingEdits?.summary ?? args.summary) && (
|
||||
<p className="text-sm font-medium text-foreground">{pendingEdits?.summary ?? args.summary}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{pendingEdits?.summary ?? args.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{((pendingEdits?.start_datetime ?? args.start_datetime) || (pendingEdits?.end_datetime ?? args.end_datetime)) && (
|
||||
{((pendingEdits?.start_datetime ?? args.start_datetime) ||
|
||||
(pendingEdits?.end_datetime ?? args.end_datetime)) && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<ClockIcon className="size-3.5 shrink-0" />
|
||||
<span>
|
||||
{(pendingEdits?.start_datetime ?? args.start_datetime) ? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime) : ""}
|
||||
{(pendingEdits?.start_datetime ?? args.start_datetime) && (pendingEdits?.end_datetime ?? args.end_datetime) ? " — " : ""}
|
||||
{(pendingEdits?.end_datetime ?? args.end_datetime) ? formatDateTime(pendingEdits?.end_datetime ?? args.end_datetime) : ""}
|
||||
{(pendingEdits?.start_datetime ?? args.start_datetime)
|
||||
? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime)
|
||||
: ""}
|
||||
{(pendingEdits?.start_datetime ?? args.start_datetime) &&
|
||||
(pendingEdits?.end_datetime ?? args.end_datetime)
|
||||
? " — "
|
||||
: ""}
|
||||
{(pendingEdits?.end_datetime ?? args.end_datetime)
|
||||
? formatDateTime(pendingEdits?.end_datetime ?? args.end_datetime)
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
MapPinIcon,
|
||||
CornerDownLeftIcon,
|
||||
} from "lucide-react";
|
||||
import { CalendarIcon, ClockIcon, CornerDownLeftIcon, MapPinIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GoogleCalendarAccount {
|
||||
|
|
@ -220,14 +215,12 @@ function ApprovalCard({
|
|||
? "Calendar Event Deletion Approved"
|
||||
: "Delete Calendar Event"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Deleting event" size="sm" />
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Event deletion was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Event deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -247,7 +240,9 @@ function ApprovalCard({
|
|||
<>
|
||||
{account && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">Google Calendar Account</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Google Calendar Account
|
||||
</p>
|
||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||
{account.name}
|
||||
</div>
|
||||
|
|
@ -315,11 +310,7 @@ function ApprovalCard({
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export { CreateCalendarEventToolUI } from "./create-event";
|
||||
export { UpdateCalendarEventToolUI } from "./update-event";
|
||||
export { DeleteCalendarEventToolUI } from "./delete-event";
|
||||
export { UpdateCalendarEventToolUI } from "./update-event";
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
ClockIcon,
|
||||
MapPinIcon,
|
||||
UsersIcon,
|
||||
ArrowRightIcon,
|
||||
ClockIcon,
|
||||
CornerDownLeftIcon,
|
||||
MapPinIcon,
|
||||
Pen,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface GoogleCalendarAccount {
|
||||
id: number;
|
||||
|
|
@ -180,8 +180,12 @@ function ApprovalCard({
|
|||
const [wasEdited, setWasEdited] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{
|
||||
summary: string; description: string; start_datetime: string;
|
||||
end_datetime: string; location: string; attendees: string;
|
||||
summary: string;
|
||||
description: string;
|
||||
start_datetime: string;
|
||||
end_datetime: string;
|
||||
location: string;
|
||||
attendees: string;
|
||||
} | null>(null);
|
||||
|
||||
const reviewConfig = interruptData.review_configs[0];
|
||||
|
|
@ -196,19 +200,21 @@ function ApprovalCard({
|
|||
const effectiveNewSummary = actionArgs.new_summary ?? args.new_summary;
|
||||
const effectiveNewStartDatetime = actionArgs.new_start_datetime ?? args.new_start_datetime;
|
||||
const effectiveNewEndDatetime = actionArgs.new_end_datetime ?? args.new_end_datetime;
|
||||
const effectiveNewLocation = actionArgs.new_location !== undefined
|
||||
? actionArgs.new_location
|
||||
: args.new_location;
|
||||
const effectiveNewAttendees = proposedAttendees
|
||||
?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
|
||||
const effectiveNewDescription = actionArgs.new_description !== undefined
|
||||
? actionArgs.new_description
|
||||
: args.new_description;
|
||||
const effectiveNewLocation =
|
||||
actionArgs.new_location !== undefined ? actionArgs.new_location : args.new_location;
|
||||
const effectiveNewAttendees =
|
||||
proposedAttendees ?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
|
||||
const effectiveNewDescription =
|
||||
actionArgs.new_description !== undefined ? actionArgs.new_description : args.new_description;
|
||||
|
||||
const changes: Array<{ label: string; oldVal: string; newVal: string }> = [];
|
||||
|
||||
if (effectiveNewSummary && String(effectiveNewSummary) !== (event?.summary ?? "")) {
|
||||
changes.push({ label: "Summary", oldVal: event?.summary ?? "", newVal: String(effectiveNewSummary) });
|
||||
changes.push({
|
||||
label: "Summary",
|
||||
oldVal: event?.summary ?? "",
|
||||
newVal: String(effectiveNewSummary),
|
||||
});
|
||||
}
|
||||
if (effectiveNewStartDatetime && String(effectiveNewStartDatetime) !== (event?.start ?? "")) {
|
||||
changes.push({
|
||||
|
|
@ -224,8 +230,15 @@ function ApprovalCard({
|
|||
newVal: formatDateTime(String(effectiveNewEndDatetime)),
|
||||
});
|
||||
}
|
||||
if (effectiveNewLocation !== undefined && String(effectiveNewLocation ?? "") !== (event?.location ?? "")) {
|
||||
changes.push({ label: "Location", oldVal: event?.location ?? "", newVal: String(effectiveNewLocation ?? "") });
|
||||
if (
|
||||
effectiveNewLocation !== undefined &&
|
||||
String(effectiveNewLocation ?? "") !== (event?.location ?? "")
|
||||
) {
|
||||
changes.push({
|
||||
label: "Location",
|
||||
oldVal: event?.location ?? "",
|
||||
newVal: String(effectiveNewLocation ?? ""),
|
||||
});
|
||||
}
|
||||
if (effectiveNewAttendees) {
|
||||
const oldStr = currentAttendees.join(", ");
|
||||
|
|
@ -242,7 +255,10 @@ function ApprovalCard({
|
|||
const buildFinalArgs = useCallback(() => {
|
||||
if (pendingEdits) {
|
||||
const attendeesArr = pendingEdits.attendees
|
||||
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
|
||||
? pendingEdits.attendees
|
||||
.split(",")
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
return {
|
||||
event_id: event?.event_id,
|
||||
|
|
@ -282,7 +298,16 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
isPanelOpen,
|
||||
allowedDecisions,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -308,15 +333,16 @@ function ApprovalCard({
|
|||
: "Update Calendar Event"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={wasEdited ? "Updating event with your changes" : "Updating event"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={wasEdited ? "Updating event with your changes" : "Updating event"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{wasEdited ? "Event updated with your changes" : "Event updated"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Event update was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Event update was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -331,24 +357,48 @@ function ApprovalCard({
|
|||
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
const proposedSummary = pendingEdits?.summary
|
||||
?? (actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
|
||||
const proposedDescription = pendingEdits?.description
|
||||
?? (actionArgs.new_description ? String(actionArgs.new_description) : (event?.description ?? ""));
|
||||
const proposedStart = pendingEdits?.start_datetime
|
||||
?? (actionArgs.new_start_datetime ? String(actionArgs.new_start_datetime) : (event?.start ?? ""));
|
||||
const proposedEnd = pendingEdits?.end_datetime
|
||||
?? (actionArgs.new_end_datetime ? String(actionArgs.new_end_datetime) : (event?.end ?? ""));
|
||||
const proposedLocation = pendingEdits?.location
|
||||
?? (actionArgs.new_location !== undefined ? String(actionArgs.new_location ?? "") : (event?.location ?? ""));
|
||||
const proposedAttendeesStr = pendingEdits?.attendees
|
||||
?? (proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
|
||||
const proposedSummary =
|
||||
pendingEdits?.summary ??
|
||||
(actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
|
||||
const proposedDescription =
|
||||
pendingEdits?.description ??
|
||||
(actionArgs.new_description
|
||||
? String(actionArgs.new_description)
|
||||
: (event?.description ?? ""));
|
||||
const proposedStart =
|
||||
pendingEdits?.start_datetime ??
|
||||
(actionArgs.new_start_datetime
|
||||
? String(actionArgs.new_start_datetime)
|
||||
: (event?.start ?? ""));
|
||||
const proposedEnd =
|
||||
pendingEdits?.end_datetime ??
|
||||
(actionArgs.new_end_datetime
|
||||
? String(actionArgs.new_end_datetime)
|
||||
: (event?.end ?? ""));
|
||||
const proposedLocation =
|
||||
pendingEdits?.location ??
|
||||
(actionArgs.new_location !== undefined
|
||||
? String(actionArgs.new_location ?? "")
|
||||
: (event?.location ?? ""));
|
||||
const proposedAttendeesStr =
|
||||
pendingEdits?.attendees ??
|
||||
(proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
|
||||
|
||||
const extraFields: ExtraField[] = [
|
||||
{ key: "start_datetime", label: "Start", type: "datetime-local", value: proposedStart },
|
||||
{
|
||||
key: "start_datetime",
|
||||
label: "Start",
|
||||
type: "datetime-local",
|
||||
value: proposedStart,
|
||||
},
|
||||
{ key: "end_datetime", label: "End", type: "datetime-local", value: proposedEnd },
|
||||
{ key: "location", label: "Location", type: "text", value: proposedLocation },
|
||||
{ key: "attendees", label: "Attendees", type: "emails", value: proposedAttendeesStr },
|
||||
{
|
||||
key: "attendees",
|
||||
label: "Attendees",
|
||||
type: "emails",
|
||||
value: proposedAttendeesStr,
|
||||
},
|
||||
];
|
||||
openHitlEditPanel({
|
||||
title: proposedSummary,
|
||||
|
|
@ -377,7 +427,7 @@ function ApprovalCard({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
{/* Content section */}
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-4 select-none">
|
||||
{context?.error ? (
|
||||
|
|
@ -433,9 +483,13 @@ function ApprovalCard({
|
|||
<div key={change.label} className="text-xs space-y-0.5">
|
||||
<span className="text-muted-foreground">{change.label}</span>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-muted-foreground line-through">{change.oldVal || "(empty)"}</span>
|
||||
<span className="text-muted-foreground line-through">
|
||||
{change.oldVal || "(empty)"}
|
||||
</span>
|
||||
<ArrowRightIcon className="size-3 text-muted-foreground shrink-0" />
|
||||
<span className="font-medium text-foreground">{change.newVal || "(empty)"}</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{change.newVal || "(empty)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -446,7 +500,8 @@ function ApprovalCard({
|
|||
className="mt-1 max-h-[5rem] overflow-hidden"
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CornerDownLeftIcon,
|
||||
FileIcon,
|
||||
Pen,
|
||||
} from "lucide-react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -15,11 +15,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface GoogleDriveAccount {
|
||||
id: number;
|
||||
|
|
@ -139,8 +135,8 @@ function ApprovalCard({
|
|||
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
|
||||
|
||||
const accounts = interruptData.context?.accounts ?? [];
|
||||
const validAccounts = accounts.filter(a => !a.auth_expired);
|
||||
const expiredAccounts = accounts.filter(a => a.auth_expired);
|
||||
const validAccounts = accounts.filter((a) => !a.auth_expired);
|
||||
const expiredAccounts = accounts.filter((a) => a.auth_expired);
|
||||
|
||||
const defaultAccountId = useMemo(() => {
|
||||
if (validAccounts.length === 1) return String(validAccounts[0].id);
|
||||
|
|
@ -162,7 +158,8 @@ function ApprovalCard({
|
|||
setParentFolderId("__root__");
|
||||
}, []);
|
||||
|
||||
const fileTypeLabel = FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
|
||||
const fileTypeLabel =
|
||||
FILE_TYPE_LABELS[selectedFileType] ?? FILE_TYPE_LABELS[args.file_type] ?? "Google Drive File";
|
||||
|
||||
const isNameValid = useMemo(() => {
|
||||
const name = pendingEdits?.name ?? args.name;
|
||||
|
|
@ -194,7 +191,20 @@ function ApprovalCard({
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, args, selectedFileType, selectedAccountId, parentFolderId, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
args,
|
||||
selectedFileType,
|
||||
selectedAccountId,
|
||||
parentFolderId,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -218,16 +228,17 @@ function ApprovalCard({
|
|||
? `${fileTypeLabel} Approved`
|
||||
: `Create ${fileTypeLabel}`}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Creating file with your changes" : "Creating file"} size="sm" />
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "File created with your changes" : "File created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
File creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -242,8 +253,8 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.name ?? (args.name ?? ""),
|
||||
content: pendingEdits?.content ?? (args.content ?? ""),
|
||||
title: pendingEdits?.name ?? args.name ?? "",
|
||||
content: pendingEdits?.content ?? args.content ?? "",
|
||||
toolName: fileTypeLabel,
|
||||
onSave: (newName, newContent) => {
|
||||
setIsPanelOpen(false);
|
||||
|
|
@ -268,33 +279,33 @@ function ApprovalCard({
|
|||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||
) : (
|
||||
<>
|
||||
{accounts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Google Drive Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={String(account.id)}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredAccounts.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{a.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{accounts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Google Drive Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validAccounts.map((account) => (
|
||||
<SelectItem key={account.id} value={String(account.id)}>
|
||||
{account.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredAccounts.map((a) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{a.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
|
|
@ -311,31 +322,29 @@ function ApprovalCard({
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedAccountId && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Parent Folder
|
||||
</p>
|
||||
<Select value={parentFolderId} onValueChange={setParentFolderId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Drive Root" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__root__">Drive Root</SelectItem>
|
||||
{availableParentFolders.map((folder) => (
|
||||
<SelectItem key={folder.folder_id} value={folder.folder_id}>
|
||||
{folder.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{availableParentFolders.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No folders found. File will be created at Drive root.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedAccountId && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
|
||||
<Select value={parentFolderId} onValueChange={setParentFolderId}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Drive Root" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__root__">Drive Root</SelectItem>
|
||||
{availableParentFolders.map((folder) => (
|
||||
<SelectItem key={folder.folder_id} value={folder.folder_id}>
|
||||
{folder.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{availableParentFolders.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No folders found. File will be created at Drive root.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -345,9 +354,11 @@ function ApprovalCard({
|
|||
{/* Content preview */}
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(pendingEdits?.name ?? args.name) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
|
||||
)}
|
||||
{(pendingEdits?.name ?? args.name) != null && (
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{String(pendingEdits?.name ?? args.name)}
|
||||
</p>
|
||||
)}
|
||||
{(pendingEdits?.content ?? args.content) != null && (
|
||||
<div
|
||||
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import {
|
||||
CornerDownLeftIcon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { CornerDownLeftIcon, InfoIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface GoogleDriveAccount {
|
||||
|
|
@ -207,14 +204,12 @@ function ApprovalCard({
|
|||
? "Google Drive File Deletion Approved"
|
||||
: "Delete Google Drive File"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Trashing file" size="sm" />
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
File deletion was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -274,22 +269,23 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 space-y-3 select-none">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The file will be moved to Google Drive trash. You can restore it from trash within 30 days.
|
||||
The file will be moved to Google Drive trash. You can restore it from trash within 30
|
||||
days.
|
||||
</p>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox
|
||||
id="delete-from-kb"
|
||||
checked={deleteFromKb}
|
||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
This will permanently delete the file from your knowledge base (cannot be undone)
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox
|
||||
id="delete-from-kb"
|
||||
checked={deleteFromKb}
|
||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
This will permanently delete the file from your knowledge base (cannot be undone)
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -298,27 +294,23 @@ function ApprovalCard({
|
|||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg text-muted-foreground"
|
||||
onClick={() => {
|
||||
setRejected();
|
||||
onDecision({ type: "reject", message: "User rejected the action." });
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="rounded-lg text-muted-foreground"
|
||||
onClick={() => {
|
||||
setRejected();
|
||||
onDecision({ type: "reject", message: "User rejected the action." });
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -11,11 +15,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface JiraAccount {
|
||||
id: number;
|
||||
|
|
@ -151,7 +151,9 @@ function ApprovalCard({
|
|||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
|
||||
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [selectedAccountId, setSelectedAccountId] = useState("");
|
||||
const [selectedProjectKey, setSelectedProjectKey] = useState(args.project_key ?? "");
|
||||
|
|
@ -177,14 +179,23 @@ function ApprovalCard({
|
|||
(overrides?: { title?: string; description?: string }) => {
|
||||
return {
|
||||
summary: overrides?.title ?? pendingEdits?.title ?? args.summary,
|
||||
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
||||
description:
|
||||
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
||||
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
|
||||
project_key: selectedProjectKey || null,
|
||||
issue_type: selectedIssueType === "__none__" ? null : selectedIssueType,
|
||||
priority: selectedPriority === "__none__" ? null : selectedPriority,
|
||||
};
|
||||
},
|
||||
[args.summary, args.description, selectedAccountId, selectedProjectKey, selectedIssueType, selectedPriority, pendingEdits]
|
||||
[
|
||||
args.summary,
|
||||
args.description,
|
||||
selectedAccountId,
|
||||
selectedProjectKey,
|
||||
selectedIssueType,
|
||||
selectedPriority,
|
||||
pendingEdits,
|
||||
]
|
||||
);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
|
|
@ -200,7 +211,17 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -225,15 +246,16 @@ function ApprovalCard({
|
|||
: "Create Jira Issue"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -248,8 +270,8 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.title ?? (args.summary ?? ""),
|
||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
||||
title: pendingEdits?.title ?? args.summary ?? "",
|
||||
content: pendingEdits?.description ?? args.description ?? "",
|
||||
toolName: "Jira Issue",
|
||||
onSave: (newTitle, newDescription) => {
|
||||
setIsPanelOpen(false);
|
||||
|
|
@ -316,10 +338,7 @@ function ApprovalCard({
|
|||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Project <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select
|
||||
value={selectedProjectKey}
|
||||
onValueChange={setSelectedProjectKey}
|
||||
>
|
||||
<Select value={selectedProjectKey} onValueChange={setSelectedProjectKey}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a project" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -336,32 +355,26 @@ function ApprovalCard({
|
|||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">Issue Type</p>
|
||||
<Select
|
||||
value={selectedIssueType}
|
||||
onValueChange={setSelectedIssueType}
|
||||
>
|
||||
<Select value={selectedIssueType} onValueChange={setSelectedIssueType}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Task" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{issueTypes.length > 0
|
||||
? issueTypes.map((t) => (
|
||||
{issueTypes.length > 0 ? (
|
||||
issueTypes.map((t) => (
|
||||
<SelectItem key={t.id} value={t.name}>
|
||||
{t.name}
|
||||
</SelectItem>
|
||||
))
|
||||
: (
|
||||
<SelectItem value="Task">Task</SelectItem>
|
||||
)}
|
||||
) : (
|
||||
<SelectItem value="Task">Task</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">Priority</p>
|
||||
<Select
|
||||
value={selectedPriority}
|
||||
onValueChange={setSelectedPriority}
|
||||
>
|
||||
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -388,7 +401,9 @@ function ApprovalCard({
|
|||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(pendingEdits?.title ?? args.summary) != null && (
|
||||
<p className="text-sm font-medium text-foreground">{pendingEdits?.title ?? args.summary}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{pendingEdits?.title ?? args.summary}
|
||||
</p>
|
||||
)}
|
||||
{(pendingEdits?.description ?? args.description) != null && (
|
||||
<div
|
||||
|
|
@ -450,9 +465,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
All Jira accounts expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">All Jira accounts expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { CornerDownLeftIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface JiraAccount {
|
||||
|
|
@ -204,9 +204,7 @@ function ApprovalCard({
|
|||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue deletion was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -280,11 +278,7 @@ function ApprovalCard({
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
@ -310,9 +304,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Jira authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -356,9 +348,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Issue not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
|||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -13,10 +16,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface JiraIssue {
|
||||
issue_id: string;
|
||||
|
|
@ -194,9 +194,12 @@ function ApprovalCard({
|
|||
const canEdit = allowedDecisions.includes("edit");
|
||||
|
||||
const hasProposedChanges =
|
||||
actionArgs.new_summary || args.new_summary ||
|
||||
actionArgs.new_description || args.new_description ||
|
||||
actionArgs.new_priority || args.new_priority;
|
||||
actionArgs.new_summary ||
|
||||
args.new_summary ||
|
||||
actionArgs.new_description ||
|
||||
args.new_description ||
|
||||
actionArgs.new_priority ||
|
||||
args.new_priority;
|
||||
|
||||
const buildFinalArgs = useCallback(() => {
|
||||
return {
|
||||
|
|
@ -222,7 +225,16 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
hasPanelEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -247,15 +259,16 @@ function ApprovalCard({
|
|||
: "Update Jira Issue"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue update was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -366,14 +379,20 @@ function ApprovalCard({
|
|||
{/* Content preview — proposed changes */}
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(hasProposedChanges || hasPanelEdits) ? (
|
||||
{hasProposedChanges || hasPanelEdits ? (
|
||||
<>
|
||||
{(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)) && (
|
||||
{(hasPanelEdits
|
||||
? editedArgs.summary
|
||||
: (actionArgs.new_summary ?? args.new_summary)) && (
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{String(hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary))}
|
||||
{String(
|
||||
hasPanelEdits ? editedArgs.summary : (actionArgs.new_summary ?? args.new_summary)
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
|
||||
{(hasPanelEdits
|
||||
? editedArgs.description
|
||||
: (actionArgs.new_description ?? args.new_description)) && (
|
||||
<div
|
||||
className="max-h-[7rem] overflow-hidden text-sm"
|
||||
style={{
|
||||
|
|
@ -382,7 +401,11 @@ function ApprovalCard({
|
|||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
|
||||
markdown={String(
|
||||
hasPanelEdits
|
||||
? editedArgs.description
|
||||
: (actionArgs.new_description ?? args.new_description)
|
||||
)}
|
||||
readOnly
|
||||
preset="readonly"
|
||||
editorVariant="none"
|
||||
|
|
@ -445,9 +468,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Jira authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -491,9 +512,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Issue not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -13,11 +17,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface LinearLabel {
|
||||
id: string;
|
||||
|
|
@ -148,7 +148,9 @@ function ApprovalCard({
|
|||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(null);
|
||||
const [pendingEdits, setPendingEdits] = useState<{ title: string; description: string } | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
||||
const [selectedTeamId, setSelectedTeamId] = useState("");
|
||||
|
|
@ -178,18 +180,32 @@ function ApprovalCard({
|
|||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||
const canEdit = allowedDecisions.includes("edit");
|
||||
|
||||
const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
|
||||
return {
|
||||
title: overrides?.title ?? pendingEdits?.title ?? args.title,
|
||||
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
||||
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
|
||||
team_id: selectedTeamId || null,
|
||||
state_id: selectedStateId === "__none__" ? null : selectedStateId,
|
||||
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
|
||||
priority: Number(selectedPriority),
|
||||
label_ids: selectedLabelIds,
|
||||
};
|
||||
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits]);
|
||||
const buildFinalArgs = useCallback(
|
||||
(overrides?: { title?: string; description?: string }) => {
|
||||
return {
|
||||
title: overrides?.title ?? pendingEdits?.title ?? args.title,
|
||||
description:
|
||||
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
||||
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
|
||||
team_id: selectedTeamId || null,
|
||||
state_id: selectedStateId === "__none__" ? null : selectedStateId,
|
||||
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
|
||||
priority: Number(selectedPriority),
|
||||
label_ids: selectedLabelIds,
|
||||
};
|
||||
},
|
||||
[
|
||||
args.title,
|
||||
args.description,
|
||||
selectedWorkspaceId,
|
||||
selectedTeamId,
|
||||
selectedStateId,
|
||||
selectedAssigneeId,
|
||||
selectedPriority,
|
||||
selectedLabelIds,
|
||||
pendingEdits,
|
||||
]
|
||||
);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
if (phase !== "pending") return;
|
||||
|
|
@ -204,7 +220,17 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
canApprove,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
pendingEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -229,15 +255,16 @@ function ApprovalCard({
|
|||
: "Create Linear Issue"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={pendingEdits ? "Creating issue with your changes" : "Creating issue"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={pendingEdits ? "Creating issue with your changes" : "Creating issue"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue creation was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -252,8 +279,8 @@ function ApprovalCard({
|
|||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
openHitlEditPanel({
|
||||
title: pendingEdits?.title ?? (args.title ?? ""),
|
||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
||||
title: pendingEdits?.title ?? args.title ?? "",
|
||||
content: pendingEdits?.description ?? args.description ?? "",
|
||||
toolName: "Linear Issue",
|
||||
onSave: (newTitle, newDescription) => {
|
||||
setIsPanelOpen(false);
|
||||
|
|
@ -269,7 +296,7 @@ function ApprovalCard({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Context section — real pickers in pending */}
|
||||
{/* Context section — real pickers in pending */}
|
||||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
|
|
@ -278,43 +305,43 @@ function ApprovalCard({
|
|||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||
) : (
|
||||
<>
|
||||
{workspaces.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Linear Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select
|
||||
value={selectedWorkspaceId}
|
||||
onValueChange={(v) => {
|
||||
setSelectedWorkspaceId(v);
|
||||
setSelectedTeamId("");
|
||||
setSelectedStateId("__none__");
|
||||
setSelectedAssigneeId("__none__");
|
||||
setSelectedPriority("0");
|
||||
setSelectedLabelIds([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validWorkspaces.map((w) => (
|
||||
<SelectItem key={w.id} value={String(w.id)}>
|
||||
{w.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredWorkspaces.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{w.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{workspaces.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Linear Account <span className="text-destructive">*</span>
|
||||
</p>
|
||||
<Select
|
||||
value={selectedWorkspaceId}
|
||||
onValueChange={(v) => {
|
||||
setSelectedWorkspaceId(v);
|
||||
setSelectedTeamId("");
|
||||
setSelectedStateId("__none__");
|
||||
setSelectedAssigneeId("__none__");
|
||||
setSelectedPriority("0");
|
||||
setSelectedLabelIds([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validWorkspaces.map((w) => (
|
||||
<SelectItem key={w.id} value={String(w.id)}>
|
||||
{w.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{expiredWorkspaces.map((w) => (
|
||||
<div
|
||||
key={w.id}
|
||||
className="relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 px-2 text-sm select-none opacity-50 pointer-events-none"
|
||||
>
|
||||
{w.name} (expired, retry after re-auth)
|
||||
</div>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedWorkspace && (
|
||||
<>
|
||||
|
|
@ -366,7 +393,10 @@ function ApprovalCard({
|
|||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">Assignee</p>
|
||||
<Select value={selectedAssigneeId} onValueChange={setSelectedAssigneeId}>
|
||||
<Select
|
||||
value={selectedAssigneeId}
|
||||
onValueChange={setSelectedAssigneeId}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Unassigned" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -520,9 +550,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
All Linear accounts expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">All Linear accounts expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { CornerDownLeftIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
|
||||
interface InterruptResult {
|
||||
|
|
@ -150,7 +150,15 @@ function ApprovalCard({
|
|||
},
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, onDecision, interruptData, issue?.id, context?.workspace?.id, deleteFromKb]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
onDecision,
|
||||
interruptData,
|
||||
issue?.id,
|
||||
context?.workspace?.id,
|
||||
deleteFromKb,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -174,14 +182,12 @@ function ApprovalCard({
|
|||
? "Linear Issue Deletion Approved"
|
||||
: "Delete Linear Issue"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text="Deleting issue" size="sm" />
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue deletion was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -232,20 +238,20 @@ function ApprovalCard({
|
|||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 select-none">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox
|
||||
id="linear-delete-from-kb"
|
||||
checked={deleteFromKb}
|
||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
This will permanently delete the issue from your knowledge base (cannot be undone)
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Checkbox
|
||||
id="linear-delete-from-kb"
|
||||
checked={deleteFromKb}
|
||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
|
||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
This will permanently delete the issue from your knowledge base (cannot be undone)
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -254,12 +260,8 @@ function ApprovalCard({
|
|||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg gap-1.5"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||
Approve
|
||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||
</Button>
|
||||
|
|
@ -285,9 +287,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Linear authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -315,9 +315,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Issue not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
|||
import { useSetAtom } from "jotai";
|
||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -14,10 +17,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||
|
||||
interface LinearLabel {
|
||||
id: string;
|
||||
|
|
@ -110,7 +110,12 @@ interface AuthErrorResult {
|
|||
connector_type: string;
|
||||
}
|
||||
|
||||
type UpdateLinearIssueResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
|
||||
type UpdateLinearIssueResult =
|
||||
| InterruptResult
|
||||
| SuccessResult
|
||||
| ErrorResult
|
||||
| NotFoundResult
|
||||
| AuthErrorResult;
|
||||
|
||||
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||
return (
|
||||
|
|
@ -178,7 +183,9 @@ function ApprovalCard({
|
|||
const issue = context?.issue;
|
||||
|
||||
const initialEditState = {
|
||||
title: actionArgs.new_title ? String(actionArgs.new_title) : (issue?.title ?? args.new_title ?? ""),
|
||||
title: actionArgs.new_title
|
||||
? String(actionArgs.new_title)
|
||||
: (issue?.title ?? args.new_title ?? ""),
|
||||
description: actionArgs.new_description
|
||||
? String(actionArgs.new_description)
|
||||
: (issue?.description ?? args.new_description ?? ""),
|
||||
|
|
@ -256,8 +263,10 @@ function ApprovalCard({
|
|||
);
|
||||
|
||||
const hasProposedChanges =
|
||||
actionArgs.new_title || args.new_title ||
|
||||
actionArgs.new_description || args.new_description ||
|
||||
actionArgs.new_title ||
|
||||
args.new_title ||
|
||||
actionArgs.new_description ||
|
||||
args.new_description ||
|
||||
proposedStateName ||
|
||||
proposedAssigneeName ||
|
||||
proposedPriorityLabel ||
|
||||
|
|
@ -276,7 +285,16 @@ function ApprovalCard({
|
|||
args: buildFinalArgs(),
|
||||
},
|
||||
});
|
||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
||||
}, [
|
||||
phase,
|
||||
setProcessing,
|
||||
isPanelOpen,
|
||||
allowedDecisions,
|
||||
onDecision,
|
||||
interruptData,
|
||||
buildFinalArgs,
|
||||
hasPanelEdits,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
|
@ -301,15 +319,16 @@ function ApprovalCard({
|
|||
: "Update Linear Issue"}
|
||||
</p>
|
||||
{phase === "processing" ? (
|
||||
<TextShimmerLoader text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"} size="sm" />
|
||||
<TextShimmerLoader
|
||||
text={hasPanelEdits ? "Updating issue with your changes" : "Updating issue"}
|
||||
size="sm"
|
||||
/>
|
||||
) : phase === "complete" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
||||
</p>
|
||||
) : phase === "rejected" ? (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Issue update was cancelled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires your approval to proceed
|
||||
|
|
@ -346,7 +365,7 @@ function ApprovalCard({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Context section — workspace + current issue + pickers in pending */}
|
||||
{/* Context section — workspace + current issue + pickers in pending */}
|
||||
{phase === "pending" && (
|
||||
<>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
|
|
@ -385,7 +404,9 @@ function ApprovalCard({
|
|||
)}
|
||||
{issue.current_assignee && <span>{issue.current_assignee.name}</span>}
|
||||
{priorities.find((p) => p.priority === issue.priority) && (
|
||||
<span>{priorities.find((p) => p.priority === issue.priority)?.label}</span>
|
||||
<span>
|
||||
{priorities.find((p) => p.priority === issue.priority)?.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{issue.current_labels && issue.current_labels.length > 0 && (
|
||||
|
|
@ -510,9 +531,7 @@ function ApprovalCard({
|
|||
? `${label.color}70`
|
||||
: `${label.color}28`,
|
||||
color: label.color,
|
||||
borderColor: isSelected
|
||||
? `${label.color}cc`
|
||||
: "transparent",
|
||||
borderColor: isSelected ? `${label.color}cc` : "transparent",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
|
|
@ -538,12 +557,18 @@ function ApprovalCard({
|
|||
{/* Content preview — proposed changes */}
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 pt-3">
|
||||
{(hasProposedChanges || hasPanelEdits) ? (
|
||||
{hasProposedChanges || hasPanelEdits ? (
|
||||
<>
|
||||
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
|
||||
<p className="text-sm font-medium text-foreground">{String(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title))}</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{String(
|
||||
hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
|
||||
{(hasPanelEdits
|
||||
? editedArgs.description
|
||||
: (actionArgs.new_description ?? args.new_description)) && (
|
||||
<div
|
||||
className="max-h-[7rem] overflow-hidden text-sm"
|
||||
style={{
|
||||
|
|
@ -552,7 +577,11 @@ function ApprovalCard({
|
|||
}}
|
||||
>
|
||||
<PlateEditor
|
||||
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
|
||||
markdown={String(
|
||||
hasPanelEdits
|
||||
? editedArgs.description
|
||||
: (actionArgs.new_description ?? args.new_description)
|
||||
)}
|
||||
readOnly
|
||||
preset="readonly"
|
||||
editorVariant="none"
|
||||
|
|
@ -641,9 +670,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-destructive">
|
||||
Linear authentication expired
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
@ -671,9 +698,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
|||
return (
|
||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||
<div className="px-5 pt-5 pb-4">
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
||||
Issue not found
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||
</div>
|
||||
<div className="mx-5 h-px bg-border/50" />
|
||||
<div className="px-5 py-4">
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue