mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 17:26:23 +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
|
# Disable Google Drive action tools if no Google Drive connector is configured
|
||||||
has_google_drive_connector = (
|
has_google_drive_connector = (
|
||||||
available_connectors is not None
|
available_connectors is not None and "GOOGLE_DRIVE_FILE" in available_connectors
|
||||||
and "GOOGLE_DRIVE_FILE" in available_connectors
|
|
||||||
)
|
)
|
||||||
if not has_google_drive_connector:
|
if not has_google_drive_connector:
|
||||||
google_drive_tools = [
|
google_drive_tools = [
|
||||||
|
|
@ -337,8 +336,7 @@ async def create_surfsense_deep_agent(
|
||||||
|
|
||||||
# Disable Jira action tools if no Jira connector is configured
|
# Disable Jira action tools if no Jira connector is configured
|
||||||
has_jira_connector = (
|
has_jira_connector = (
|
||||||
available_connectors is not None
|
available_connectors is not None and "JIRA_CONNECTOR" in available_connectors
|
||||||
and "JIRA_CONNECTOR" in available_connectors
|
|
||||||
)
|
)
|
||||||
if not has_jira_connector:
|
if not has_jira_connector:
|
||||||
jira_tools = [
|
jira_tools = [
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,16 @@ def create_create_confluence_page_tool(
|
||||||
logger.info(f"create_confluence_page called: title='{title}'")
|
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:
|
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:
|
try:
|
||||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
return {"status": "error", "message": context["error"]}
|
return {"status": "error", "message": context["error"]}
|
||||||
|
|
@ -60,22 +65,28 @@ def create_create_confluence_page_tool(
|
||||||
"connector_type": "confluence",
|
"connector_type": "confluence",
|
||||||
}
|
}
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "confluence_page_creation",
|
{
|
||||||
"action": {
|
"type": "confluence_page_creation",
|
||||||
"tool": "create_confluence_page",
|
"action": {
|
||||||
"params": {
|
"tool": "create_confluence_page",
|
||||||
"title": title,
|
"params": {
|
||||||
"content": content,
|
"title": title,
|
||||||
"space_id": space_id,
|
"content": content,
|
||||||
"connector_id": connector_id,
|
"space_id": space_id,
|
||||||
|
"connector_id": connector_id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"context": context,
|
||||||
"context": context,
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
decisions_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
edited_action = decision.get("edited_action")
|
||||||
|
|
@ -114,12 +128,16 @@ def create_create_confluence_page_tool(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "No Confluence connector found."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "No Confluence connector found.",
|
||||||
|
}
|
||||||
actual_connector_id = connector.id
|
actual_connector_id = connector.id
|
||||||
else:
|
else:
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
|
|
@ -127,15 +145,21 @@ def create_create_confluence_page_tool(
|
||||||
SearchSourceConnector.id == actual_connector_id,
|
SearchSourceConnector.id == actual_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Selected Confluence connector is invalid.",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
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(
|
api_result = await client.create_page(
|
||||||
space_id=final_space_id,
|
space_id=final_space_id,
|
||||||
title=final_title,
|
title=final_title,
|
||||||
|
|
@ -143,7 +167,10 @@ def create_create_confluence_page_tool(
|
||||||
)
|
)
|
||||||
await client.close()
|
await client.close()
|
||||||
except Exception as api_err:
|
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:
|
try:
|
||||||
_conn = connector
|
_conn = connector
|
||||||
_conn.config = {**_conn.config, "auth_expired": True}
|
_conn.config = {**_conn.config, "auth_expired": True}
|
||||||
|
|
@ -163,6 +190,7 @@ def create_create_confluence_page_tool(
|
||||||
kb_message_suffix = ""
|
kb_message_suffix = ""
|
||||||
try:
|
try:
|
||||||
from app.services.confluence import ConfluenceKBSyncService
|
from app.services.confluence import ConfluenceKBSyncService
|
||||||
|
|
||||||
kb_service = ConfluenceKBSyncService(db_session)
|
kb_service = ConfluenceKBSyncService(db_session)
|
||||||
kb_result = await kb_service.sync_after_create(
|
kb_result = await kb_service.sync_after_create(
|
||||||
page_id=page_id,
|
page_id=page_id,
|
||||||
|
|
@ -189,9 +217,13 @@ def create_create_confluence_page_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error creating Confluence page: {e}", exc_info=True)
|
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
|
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 "not_found", relay the message to the user.
|
||||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
- 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:
|
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:
|
try:
|
||||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
error_msg = context["error"]
|
error_msg = context["error"]
|
||||||
|
|
@ -67,21 +74,27 @@ def create_delete_confluence_page_tool(
|
||||||
document_id = page_data["document_id"]
|
document_id = page_data["document_id"]
|
||||||
connector_id_from_context = context.get("account", {}).get("id")
|
connector_id_from_context = context.get("account", {}).get("id")
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "confluence_page_deletion",
|
{
|
||||||
"action": {
|
"type": "confluence_page_deletion",
|
||||||
"tool": "delete_confluence_page",
|
"action": {
|
||||||
"params": {
|
"tool": "delete_confluence_page",
|
||||||
"page_id": page_id,
|
"params": {
|
||||||
"connector_id": connector_id_from_context,
|
"page_id": page_id,
|
||||||
"delete_from_kb": delete_from_kb,
|
"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_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
edited_action = decision.get("edited_action")
|
||||||
|
|
@ -102,33 +118,47 @@ def create_delete_confluence_page_tool(
|
||||||
final_params = decision["args"]
|
final_params = decision["args"]
|
||||||
|
|
||||||
final_page_id = final_params.get("page_id", page_id)
|
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)
|
final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb)
|
||||||
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||||
|
|
||||||
if not final_connector_id:
|
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(
|
result = await db_session.execute(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.id == final_connector_id,
|
SearchSourceConnector.id == final_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Selected Confluence connector is invalid.",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
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.delete_page(final_page_id)
|
||||||
await client.close()
|
await client.close()
|
||||||
except Exception as api_err:
|
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:
|
try:
|
||||||
connector.config = {**connector.config, "auth_expired": True}
|
connector.config = {**connector.config, "auth_expired": True}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
|
|
@ -146,6 +176,7 @@ def create_delete_confluence_page_tool(
|
||||||
if final_delete_from_kb and document_id:
|
if final_delete_from_kb and document_id:
|
||||||
try:
|
try:
|
||||||
from app.db import Document
|
from app.db import Document
|
||||||
|
|
||||||
doc_result = await db_session.execute(
|
doc_result = await db_session.execute(
|
||||||
select(Document).filter(Document.id == document_id)
|
select(Document).filter(Document.id == document_id)
|
||||||
)
|
)
|
||||||
|
|
@ -171,9 +202,13 @@ def create_delete_confluence_page_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error deleting Confluence page: {e}", exc_info=True)
|
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
|
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 "not_found", relay the message to the user.
|
||||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
- 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:
|
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:
|
try:
|
||||||
metadata_service = ConfluenceToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
error_msg = context["error"]
|
error_msg = context["error"]
|
||||||
|
|
@ -71,24 +78,30 @@ def create_update_confluence_page_tool(
|
||||||
document_id = page_data.get("document_id")
|
document_id = page_data.get("document_id")
|
||||||
connector_id_from_context = context.get("account", {}).get("id")
|
connector_id_from_context = context.get("account", {}).get("id")
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "confluence_page_update",
|
{
|
||||||
"action": {
|
"type": "confluence_page_update",
|
||||||
"tool": "update_confluence_page",
|
"action": {
|
||||||
"params": {
|
"tool": "update_confluence_page",
|
||||||
"page_id": page_id,
|
"params": {
|
||||||
"document_id": document_id,
|
"page_id": page_id,
|
||||||
"new_title": new_title,
|
"document_id": document_id,
|
||||||
"new_content": new_content,
|
"new_title": new_title,
|
||||||
"version": current_version,
|
"new_content": new_content,
|
||||||
"connector_id": connector_id_from_context,
|
"version": current_version,
|
||||||
|
"connector_id": connector_id_from_context,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"context": context,
|
||||||
"context": context,
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
decisions_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
edited_action = decision.get("edited_action")
|
||||||
|
|
@ -114,29 +130,40 @@ def create_update_confluence_page_tool(
|
||||||
if final_content is None:
|
if final_content is None:
|
||||||
final_content = current_body
|
final_content = current_body
|
||||||
final_version = final_params.get("version", current_version)
|
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)
|
final_document_id = final_params.get("document_id", document_id)
|
||||||
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||||
|
|
||||||
if not final_connector_id:
|
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(
|
result = await db_session.execute(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.id == final_connector_id,
|
SearchSourceConnector.id == final_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.CONFLUENCE_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "Selected Confluence connector is invalid."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Selected Confluence connector is invalid.",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
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(
|
await client.update_page(
|
||||||
page_id=final_page_id,
|
page_id=final_page_id,
|
||||||
title=final_title,
|
title=final_title,
|
||||||
|
|
@ -145,7 +172,10 @@ def create_update_confluence_page_tool(
|
||||||
)
|
)
|
||||||
await client.close()
|
await client.close()
|
||||||
except Exception as api_err:
|
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:
|
try:
|
||||||
connector.config = {**connector.config, "auth_expired": True}
|
connector.config = {**connector.config, "auth_expired": True}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
|
|
@ -163,6 +193,7 @@ def create_update_confluence_page_tool(
|
||||||
if final_document_id:
|
if final_document_id:
|
||||||
try:
|
try:
|
||||||
from app.services.confluence import ConfluenceKBSyncService
|
from app.services.confluence import ConfluenceKBSyncService
|
||||||
|
|
||||||
kb_service = ConfluenceKBSyncService(db_session)
|
kb_service = ConfluenceKBSyncService(db_session)
|
||||||
kb_result = await kb_service.sync_after_update(
|
kb_result = await kb_service.sync_after_update(
|
||||||
document_id=final_document_id,
|
document_id=final_document_id,
|
||||||
|
|
@ -171,12 +202,18 @@ def create_update_confluence_page_tool(
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
)
|
)
|
||||||
if kb_result["status"] == "success":
|
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:
|
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:
|
except Exception as kb_err:
|
||||||
logger.warning(f"KB sync after update failed: {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 {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|
@ -186,9 +223,13 @@ def create_update_confluence_page_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error updating Confluence page: {e}", exc_info=True)
|
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
|
return update_confluence_page
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,7 @@ def create_create_gmail_draft_tool(
|
||||||
- "Draft an email to alice@example.com about the meeting"
|
- "Draft an email to alice@example.com about the meeting"
|
||||||
- "Compose a reply to Bob about the project update"
|
- "Compose a reply to Bob about the project update"
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(f"create_gmail_draft called: to='{to}', subject='{subject}'")
|
||||||
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:
|
if db_session is None or search_space_id is None or user_id is None:
|
||||||
return {
|
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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
|
|
@ -251,10 +252,12 @@ def create_create_gmail_draft_tool(
|
||||||
try:
|
try:
|
||||||
created = await asyncio.get_event_loop().run_in_executor(
|
created = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: gmail_service.users()
|
lambda: (
|
||||||
.drafts()
|
gmail_service.users()
|
||||||
.create(userId="me", body={"message": {"raw": raw}})
|
.drafts()
|
||||||
.execute(),
|
.create(userId="me", body={"message": {"raw": raw}})
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
@ -289,9 +292,7 @@ def create_create_gmail_draft_tool(
|
||||||
}
|
}
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Gmail draft created: id={created.get('id')}")
|
||||||
f"Gmail draft created: id={created.get('id')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
kb_message_suffix = ""
|
kb_message_suffix = ""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,7 @@ def create_send_gmail_email_tool(
|
||||||
- "Send an email to alice@example.com about the meeting"
|
- "Send an email to alice@example.com about the meeting"
|
||||||
- "Email Bob the project update"
|
- "Email Bob the project update"
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(f"send_gmail_email called: to='{to}', subject='{subject}'")
|
||||||
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:
|
if db_session is None or search_space_id is None or user_id is None:
|
||||||
return {
|
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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
|
|
@ -252,10 +253,12 @@ def create_send_gmail_email_tool(
|
||||||
try:
|
try:
|
||||||
sent = await asyncio.get_event_loop().run_in_executor(
|
sent = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: gmail_service.users()
|
lambda: (
|
||||||
.messages()
|
gmail_service.users()
|
||||||
.send(userId="me", body={"raw": raw})
|
.messages()
|
||||||
.execute(),
|
.send(userId="me", body={"raw": raw})
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
|
|
@ -241,10 +244,12 @@ def create_trash_gmail_email_tool(
|
||||||
try:
|
try:
|
||||||
await asyncio.get_event_loop().run_in_executor(
|
await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: gmail_service.users()
|
lambda: (
|
||||||
.messages()
|
gmail_service.users()
|
||||||
.trash(userId="me", id=final_message_id)
|
.messages()
|
||||||
.execute(),
|
.trash(userId="me", id=final_message_id)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
@ -257,7 +262,10 @@ def create_trash_gmail_email_tool(
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
if not connector.config.get("auth_expired"):
|
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")
|
flag_modified(connector, "config")
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -273,9 +281,7 @@ def create_trash_gmail_email_tool(
|
||||||
}
|
}
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Gmail email trashed: message_id={final_message_id}")
|
||||||
f"Gmail email trashed: message_id={final_message_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
trash_result: dict[str, Any] = {
|
trash_result: dict[str, Any] = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,10 @@ def create_update_gmail_draft_tool(
|
||||||
f"Updating Gmail draft: subject='{final_subject}', connector={final_connector_id}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
|
|
@ -299,14 +302,16 @@ def create_update_gmail_draft_tool(
|
||||||
try:
|
try:
|
||||||
updated = await asyncio.get_event_loop().run_in_executor(
|
updated = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: gmail_service.users()
|
lambda: (
|
||||||
.drafts()
|
gmail_service.users()
|
||||||
.update(
|
.drafts()
|
||||||
userId="me",
|
.update(
|
||||||
id=final_draft_id,
|
userId="me",
|
||||||
body={"message": {"raw": raw}},
|
id=final_draft_id,
|
||||||
)
|
body={"message": {"raw": raw}},
|
||||||
.execute(),
|
)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
@ -369,7 +374,9 @@ def create_update_gmail_draft_tool(
|
||||||
document.document_metadata = meta
|
document.document_metadata = meta
|
||||||
flag_modified(document, "document_metadata")
|
flag_modified(document, "document_metadata")
|
||||||
await db_session.commit()
|
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(
|
logger.info(
|
||||||
f"KB document {document_id} updated for draft {final_draft_id}"
|
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", [])
|
accounts = context.get("accounts", [])
|
||||||
if accounts and all(a.get("auth_expired") for a in 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 {
|
return {
|
||||||
"status": "auth_error",
|
"status": "auth_error",
|
||||||
"message": "All connected Google Calendar accounts need re-authentication. Please re-authenticate in your connector settings.",
|
"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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
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)
|
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||||
for key in ("token", "refresh_token", "client_secret"):
|
for key in ("token", "refresh_token", "client_secret"):
|
||||||
if config_data.get(key):
|
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", "")
|
exp = config_data.get("expiry", "")
|
||||||
if exp:
|
if exp:
|
||||||
|
|
@ -254,9 +261,11 @@ def create_create_calendar_event_tool(
|
||||||
try:
|
try:
|
||||||
created = await asyncio.get_event_loop().run_in_executor(
|
created = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.events()
|
lambda: (
|
||||||
.insert(calendarId="primary", body=event_body)
|
service.events()
|
||||||
.execute(),
|
.insert(calendarId="primary", body=event_body)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
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)
|
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||||
for key in ("token", "refresh_token", "client_secret"):
|
for key in ("token", "refresh_token", "client_secret"):
|
||||||
if config_data.get(key):
|
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", "")
|
exp = config_data.get("expiry", "")
|
||||||
if exp:
|
if exp:
|
||||||
|
|
@ -232,9 +237,11 @@ def create_delete_calendar_event_tool(
|
||||||
try:
|
try:
|
||||||
await asyncio.get_event_loop().run_in_executor(
|
await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.events()
|
lambda: (
|
||||||
.delete(calendarId="primary", eventId=final_event_id)
|
service.events()
|
||||||
.execute(),
|
.delete(calendarId="primary", eventId=final_event_id)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
@ -269,9 +276,7 @@ def create_delete_calendar_event_tool(
|
||||||
}
|
}
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Calendar event deleted: event_id={final_event_id}")
|
||||||
f"Calendar event deleted: event_id={final_event_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
delete_result: dict[str, Any] = {
|
delete_result: dict[str, Any] = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@ def create_update_calendar_event_tool(
|
||||||
- "Reschedule the team standup to 3pm"
|
- "Reschedule the team standup to 3pm"
|
||||||
- "Change the location of my dentist appointment"
|
- "Change the location of my dentist appointment"
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(f"update_calendar_event called: event_ref='{event_title_or_id}'")
|
||||||
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:
|
if db_session is None or search_space_id is None or user_id is None:
|
||||||
return {
|
return {
|
||||||
|
|
@ -83,9 +81,7 @@ def create_update_calendar_event_tool(
|
||||||
return {"status": "error", "message": error_msg}
|
return {"status": "error", "message": error_msg}
|
||||||
|
|
||||||
if context.get("auth_expired"):
|
if context.get("auth_expired"):
|
||||||
logger.warning(
|
logger.warning("Google Calendar account has expired authentication")
|
||||||
"Google Calendar account has expired authentication"
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
"status": "auth_error",
|
"status": "auth_error",
|
||||||
"message": "The Google Calendar account for this event needs re-authentication. Please re-authenticate in your connector settings.",
|
"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
|
"connector_id", connector_id_from_context
|
||||||
)
|
)
|
||||||
final_new_summary = final_params.get("new_summary", new_summary)
|
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_start_datetime = final_params.get(
|
||||||
final_new_end_datetime = final_params.get("new_end_datetime", new_end_datetime)
|
"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_description = final_params.get("new_description", new_description)
|
||||||
final_new_location = final_params.get("new_location", new_location)
|
final_new_location = final_params.get("new_location", new_location)
|
||||||
final_new_attendees = final_params.get("new_attendees", new_attendees)
|
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}"
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
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)
|
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||||
for key in ("token", "refresh_token", "client_secret"):
|
for key in ("token", "refresh_token", "client_secret"):
|
||||||
if config_data.get(key):
|
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", "")
|
exp = config_data.get("expiry", "")
|
||||||
if exp:
|
if exp:
|
||||||
|
|
@ -250,11 +255,25 @@ def create_update_calendar_event_tool(
|
||||||
if final_new_summary is not None:
|
if final_new_summary is not None:
|
||||||
update_body["summary"] = final_new_summary
|
update_body["summary"] = final_new_summary
|
||||||
if final_new_start_datetime is not None:
|
if final_new_start_datetime is not None:
|
||||||
tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC"
|
tz = (
|
||||||
update_body["start"] = {"dateTime": final_new_start_datetime, "timeZone": 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:
|
if final_new_end_datetime is not None:
|
||||||
tz = context.get("timezone", "UTC") if isinstance(context, dict) else "UTC"
|
tz = (
|
||||||
update_body["end"] = {"dateTime": final_new_end_datetime, "timeZone": 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:
|
if final_new_description is not None:
|
||||||
update_body["description"] = final_new_description
|
update_body["description"] = final_new_description
|
||||||
if final_new_location is not None:
|
if final_new_location is not None:
|
||||||
|
|
@ -273,9 +292,15 @@ def create_update_calendar_event_tool(
|
||||||
try:
|
try:
|
||||||
updated = await asyncio.get_event_loop().run_in_executor(
|
updated = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.events()
|
lambda: (
|
||||||
.patch(calendarId="primary", eventId=final_event_id, body=update_body)
|
service.events()
|
||||||
.execute(),
|
.patch(
|
||||||
|
calendarId="primary",
|
||||||
|
eventId=final_event_id,
|
||||||
|
body=update_body,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
@ -310,9 +335,7 @@ def create_update_calendar_event_tool(
|
||||||
}
|
}
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Calendar event updated: event_id={final_event_id}")
|
||||||
f"Calendar event updated: event_id={final_event_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
kb_message_suffix = ""
|
kb_message_suffix = ""
|
||||||
if document_id is not None:
|
if document_id is not None:
|
||||||
|
|
@ -328,7 +351,9 @@ def create_update_calendar_event_tool(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
if kb_result["status"] == "success":
|
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:
|
else:
|
||||||
kb_message_suffix = " The knowledge base will be updated in the next scheduled sync."
|
kb_message_suffix = " The knowledge base will be updated in the next scheduled sync."
|
||||||
except Exception as kb_err:
|
except Exception as kb_err:
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,10 @@ def create_create_google_drive_file_tool(
|
||||||
)
|
)
|
||||||
|
|
||||||
pre_built_creds = None
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,10 @@ def create_delete_google_drive_file_tool(
|
||||||
)
|
)
|
||||||
|
|
||||||
pre_built_creds = None
|
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
|
from app.utils.google_credentials import build_composio_credentials
|
||||||
|
|
||||||
cca_id = connector.config.get("composio_connected_account_id")
|
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
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
if not connector.config.get("auth_expired"):
|
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")
|
flag_modified(connector, "config")
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
except Exception:
|
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 "rejected", the user declined. Do NOT retry.
|
||||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
- 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:
|
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."}
|
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata_service = JiraToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
return {"status": "error", "message": context["error"]}
|
return {"status": "error", "message": context["error"]}
|
||||||
|
|
@ -65,24 +69,30 @@ def create_create_jira_issue_tool(
|
||||||
"connector_type": "jira",
|
"connector_type": "jira",
|
||||||
}
|
}
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "jira_issue_creation",
|
{
|
||||||
"action": {
|
"type": "jira_issue_creation",
|
||||||
"tool": "create_jira_issue",
|
"action": {
|
||||||
"params": {
|
"tool": "create_jira_issue",
|
||||||
"project_key": project_key,
|
"params": {
|
||||||
"summary": summary,
|
"project_key": project_key,
|
||||||
"issue_type": issue_type,
|
"summary": summary,
|
||||||
"description": description,
|
"issue_type": issue_type,
|
||||||
"priority": priority,
|
"description": description,
|
||||||
"connector_id": connector_id,
|
"priority": priority,
|
||||||
|
"connector_id": connector_id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
"context": context,
|
||||||
"context": context,
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
decisions_raw = approval.get("decisions", []) if isinstance(approval, dict) else []
|
decisions_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
edited_action = decision.get("edited_action")
|
||||||
|
|
@ -123,7 +136,8 @@ def create_create_jira_issue_tool(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
|
|
@ -136,15 +150,21 @@ def create_create_jira_issue_tool(
|
||||||
SearchSourceConnector.id == actual_connector_id,
|
SearchSourceConnector.id == actual_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "Selected Jira connector is invalid."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Selected Jira connector is invalid.",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
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()
|
jira_client = await jira_history._get_jira_client()
|
||||||
api_result = await asyncio.to_thread(
|
api_result = await asyncio.to_thread(
|
||||||
jira_client.create_issue,
|
jira_client.create_issue,
|
||||||
|
|
@ -175,6 +195,7 @@ def create_create_jira_issue_tool(
|
||||||
kb_message_suffix = ""
|
kb_message_suffix = ""
|
||||||
try:
|
try:
|
||||||
from app.services.jira import JiraKBSyncService
|
from app.services.jira import JiraKBSyncService
|
||||||
|
|
||||||
kb_service = JiraKBSyncService(db_session)
|
kb_service = JiraKBSyncService(db_session)
|
||||||
kb_result = await kb_service.sync_after_create(
|
kb_result = await kb_service.sync_after_create(
|
||||||
issue_id=issue_key,
|
issue_id=issue_key,
|
||||||
|
|
@ -202,9 +223,13 @@ def create_create_jira_issue_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error creating Jira issue: {e}", exc_info=True)
|
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
|
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 "not_found", relay the message to the user.
|
||||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
- 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:
|
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."}
|
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata_service = JiraToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
error_msg = context["error"]
|
error_msg = context["error"]
|
||||||
|
|
@ -67,21 +71,27 @@ def create_delete_jira_issue_tool(
|
||||||
document_id = issue_data["document_id"]
|
document_id = issue_data["document_id"]
|
||||||
connector_id_from_context = context.get("account", {}).get("id")
|
connector_id_from_context = context.get("account", {}).get("id")
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "jira_issue_deletion",
|
{
|
||||||
"action": {
|
"type": "jira_issue_deletion",
|
||||||
"tool": "delete_jira_issue",
|
"action": {
|
||||||
"params": {
|
"tool": "delete_jira_issue",
|
||||||
"issue_key": issue_key,
|
"params": {
|
||||||
"connector_id": connector_id_from_context,
|
"issue_key": issue_key,
|
||||||
"delete_from_kb": delete_from_kb,
|
"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_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
edited_action = decision.get("edited_action")
|
||||||
|
|
@ -102,29 +115,40 @@ def create_delete_jira_issue_tool(
|
||||||
final_params = decision["args"]
|
final_params = decision["args"]
|
||||||
|
|
||||||
final_issue_key = final_params.get("issue_key", issue_key)
|
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)
|
final_delete_from_kb = final_params.get("delete_from_kb", delete_from_kb)
|
||||||
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||||
|
|
||||||
if not final_connector_id:
|
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(
|
result = await db_session.execute(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.id == final_connector_id,
|
SearchSourceConnector.id == final_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
if not connector:
|
||||||
return {"status": "error", "message": "Selected Jira connector is invalid."}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "Selected Jira connector is invalid.",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
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()
|
jira_client = await jira_history._get_jira_client()
|
||||||
await asyncio.to_thread(jira_client.delete_issue, final_issue_key)
|
await asyncio.to_thread(jira_client.delete_issue, final_issue_key)
|
||||||
except Exception as api_err:
|
except Exception as api_err:
|
||||||
|
|
@ -146,6 +170,7 @@ def create_delete_jira_issue_tool(
|
||||||
if final_delete_from_kb and document_id:
|
if final_delete_from_kb and document_id:
|
||||||
try:
|
try:
|
||||||
from app.db import Document
|
from app.db import Document
|
||||||
|
|
||||||
doc_result = await db_session.execute(
|
doc_result = await db_session.execute(
|
||||||
select(Document).filter(Document.id == document_id)
|
select(Document).filter(Document.id == document_id)
|
||||||
)
|
)
|
||||||
|
|
@ -171,9 +196,13 @@ def create_delete_jira_issue_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error deleting Jira issue: {e}", exc_info=True)
|
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
|
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 "not_found", relay the message and ask user to verify.
|
||||||
- If status is "insufficient_permissions", inform user to re-authenticate.
|
- 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:
|
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."}
|
return {"status": "error", "message": "Jira tool not properly configured."}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata_service = JiraToolMetadataService(db_session)
|
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:
|
if "error" in context:
|
||||||
error_msg = context["error"]
|
error_msg = context["error"]
|
||||||
|
|
@ -71,24 +75,30 @@ def create_update_jira_issue_tool(
|
||||||
document_id = issue_data.get("document_id")
|
document_id = issue_data.get("document_id")
|
||||||
connector_id_from_context = context.get("account", {}).get("id")
|
connector_id_from_context = context.get("account", {}).get("id")
|
||||||
|
|
||||||
approval = interrupt({
|
approval = interrupt(
|
||||||
"type": "jira_issue_update",
|
{
|
||||||
"action": {
|
"type": "jira_issue_update",
|
||||||
"tool": "update_jira_issue",
|
"action": {
|
||||||
"params": {
|
"tool": "update_jira_issue",
|
||||||
"issue_key": issue_key,
|
"params": {
|
||||||
"document_id": document_id,
|
"issue_key": issue_key,
|
||||||
"new_summary": new_summary,
|
"document_id": document_id,
|
||||||
"new_description": new_description,
|
"new_summary": new_summary,
|
||||||
"new_priority": new_priority,
|
"new_description": new_description,
|
||||||
"connector_id": connector_id_from_context,
|
"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_raw = (
|
||||||
decisions = decisions_raw if isinstance(decisions_raw, list) else [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)]
|
decisions = [d for d in decisions if isinstance(d, dict)]
|
||||||
if not decisions:
|
if not decisions:
|
||||||
return {"status": "error", "message": "No approval decision received"}
|
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")
|
decision_type = decision.get("type") or decision.get("decision_type")
|
||||||
|
|
||||||
if decision_type == "reject":
|
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] = {}
|
final_params: dict[str, Any] = {}
|
||||||
edited_action = decision.get("edited_action")
|
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_summary = final_params.get("new_summary", new_summary)
|
||||||
final_description = final_params.get("new_description", new_description)
|
final_description = final_params.get("new_description", new_description)
|
||||||
final_priority = final_params.get("new_priority", new_priority)
|
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)
|
final_document_id = final_params.get("document_id", document_id)
|
||||||
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
from app.db import SearchSourceConnector, SearchSourceConnectorType
|
||||||
|
|
||||||
if not final_connector_id:
|
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(
|
result = await db_session.execute(
|
||||||
select(SearchSourceConnector).filter(
|
select(SearchSourceConnector).filter(
|
||||||
SearchSourceConnector.id == final_connector_id,
|
SearchSourceConnector.id == final_connector_id,
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type == SearchSourceConnectorType.JIRA_CONNECTOR,
|
SearchSourceConnector.connector_type
|
||||||
|
== SearchSourceConnectorType.JIRA_CONNECTOR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
connector = result.scalars().first()
|
connector = result.scalars().first()
|
||||||
if not connector:
|
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] = {}
|
fields: dict[str, Any] = {}
|
||||||
if final_summary:
|
if final_summary:
|
||||||
|
|
@ -140,7 +162,12 @@ def create_update_jira_issue_tool(
|
||||||
fields["description"] = {
|
fields["description"] = {
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"content": [{"type": "paragraph", "content": [{"type": "text", "text": final_description}]}],
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [{"type": "text", "text": final_description}],
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
if final_priority:
|
if final_priority:
|
||||||
fields["priority"] = {"name": final_priority}
|
fields["priority"] = {"name": final_priority}
|
||||||
|
|
@ -149,9 +176,13 @@ def create_update_jira_issue_tool(
|
||||||
return {"status": "error", "message": "No changes specified."}
|
return {"status": "error", "message": "No changes specified."}
|
||||||
|
|
||||||
try:
|
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()
|
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:
|
except Exception as api_err:
|
||||||
if "status code 403" in str(api_err).lower():
|
if "status code 403" in str(api_err).lower():
|
||||||
try:
|
try:
|
||||||
|
|
@ -171,6 +202,7 @@ def create_update_jira_issue_tool(
|
||||||
if final_document_id:
|
if final_document_id:
|
||||||
try:
|
try:
|
||||||
from app.services.jira import JiraKBSyncService
|
from app.services.jira import JiraKBSyncService
|
||||||
|
|
||||||
kb_service = JiraKBSyncService(db_session)
|
kb_service = JiraKBSyncService(db_session)
|
||||||
kb_result = await kb_service.sync_after_update(
|
kb_result = await kb_service.sync_after_update(
|
||||||
document_id=final_document_id,
|
document_id=final_document_id,
|
||||||
|
|
@ -179,12 +211,18 @@ def create_update_jira_issue_tool(
|
||||||
search_space_id=search_space_id,
|
search_space_id=search_space_id,
|
||||||
)
|
)
|
||||||
if kb_result["status"] == "success":
|
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:
|
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:
|
except Exception as kb_err:
|
||||||
logger.warning(f"KB sync after update failed: {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 {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|
@ -194,9 +232,13 @@ def create_update_jira_issue_tool(
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from langgraph.errors import GraphInterrupt
|
from langgraph.errors import GraphInterrupt
|
||||||
|
|
||||||
if isinstance(e, GraphInterrupt):
|
if isinstance(e, GraphInterrupt):
|
||||||
raise
|
raise
|
||||||
logger.error(f"Error updating Jira issue: {e}", exc_info=True)
|
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
|
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).
|
# a real search. We want breadth (many docs) over depth (many chunks).
|
||||||
_BROWSE_MAX_CHUNKS_PER_DOC = 5
|
_BROWSE_MAX_CHUNKS_PER_DOC = 5
|
||||||
|
|
||||||
|
|
||||||
def _is_degenerate_query(query: str) -> bool:
|
def _is_degenerate_query(query: str) -> bool:
|
||||||
"""Return True when the query carries no meaningful search signal.
|
"""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]
|
base_conditions = [Document.search_space_id == search_space_id]
|
||||||
|
|
||||||
if document_type is not None:
|
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 = []
|
doc_type_enums = []
|
||||||
for dt in type_list:
|
for dt in type_list:
|
||||||
if isinstance(dt, str):
|
if isinstance(dt, str):
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,9 @@ def create_create_notion_page_tool(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
if kb_result["status"] == "success":
|
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:
|
else:
|
||||||
kb_message_suffix = " This page will be added to your knowledge base in the next scheduled sync."
|
kb_message_suffix = " This page will be added to your knowledge base in the next scheduled sync."
|
||||||
except Exception as kb_err:
|
except Exception as kb_err:
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,9 @@ def create_delete_notion_page_tool(
|
||||||
return {
|
return {
|
||||||
"status": "auth_error",
|
"status": "auth_error",
|
||||||
"message": str(e),
|
"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",
|
"connector_type": "notion",
|
||||||
}
|
}
|
||||||
if isinstance(e, ValueError | NotionAPIError):
|
if isinstance(e, ValueError | NotionAPIError):
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,9 @@ def create_update_notion_page_tool(
|
||||||
return {
|
return {
|
||||||
"status": "auth_error",
|
"status": "auth_error",
|
||||||
"message": str(e),
|
"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",
|
"connector_type": "notion",
|
||||||
}
|
}
|
||||||
if isinstance(e, ValueError | NotionAPIError):
|
if isinstance(e, ValueError | NotionAPIError):
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,7 @@ if config.NEXT_FRONTEND_URL:
|
||||||
allowed_origins.append(www_url)
|
allowed_origins.append(www_url)
|
||||||
|
|
||||||
allowed_origins.extend(
|
allowed_origins.extend(
|
||||||
[ # For local development and desktop app
|
[ # For local development and desktop app
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
"http://127.0.0.1:3000",
|
"http://127.0.0.1:3000",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,9 @@ class ConfluenceHistoryConnector:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Lazy import to avoid circular dependency
|
# 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)
|
connector = await refresh_confluence_token(self._session, connector)
|
||||||
|
|
||||||
|
|
@ -375,13 +377,9 @@ class ConfluenceHistoryConnector:
|
||||||
url, headers=headers, json=json_payload, params=params
|
url, headers=headers, json=json_payload, params=params
|
||||||
)
|
)
|
||||||
elif method_upper == "DELETE":
|
elif method_upper == "DELETE":
|
||||||
response = await http_client.delete(
|
response = await http_client.delete(url, headers=headers, params=params)
|
||||||
url, headers=headers, params=params
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
response = await http_client.get(
|
response = await http_client.get(url, headers=headers, params=params)
|
||||||
url, headers=headers, params=params
|
|
||||||
)
|
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if response.status_code == 204 or not response.text:
|
if response.status_code == 204 or not response.text:
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,7 @@ class GoogleCalendarConnector:
|
||||||
has_standard_refresh = bool(self._credentials.refresh_token)
|
has_standard_refresh = bool(self._credentials.refresh_token)
|
||||||
|
|
||||||
if has_standard_refresh:
|
if has_standard_refresh:
|
||||||
if not all(
|
if not all([self._credentials.client_id, self._credentials.client_secret]):
|
||||||
[self._credentials.client_id, self._credentials.client_secret]
|
|
||||||
):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Google OAuth credentials (client_id, client_secret) must be set"
|
"Google OAuth credentials (client_id, client_secret) must be set"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,7 @@ class GoogleGmailConnector:
|
||||||
has_standard_refresh = bool(self._credentials.refresh_token)
|
has_standard_refresh = bool(self._credentials.refresh_token)
|
||||||
|
|
||||||
if has_standard_refresh:
|
if has_standard_refresh:
|
||||||
if not all(
|
if not all([self._credentials.client_id, self._credentials.client_secret]):
|
||||||
[self._credentials.client_id, self._credentials.client_secret]
|
|
||||||
):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Google OAuth credentials (client_id, client_secret) must be set"
|
"Google OAuth credentials (client_id, client_secret) must be set"
|
||||||
)
|
)
|
||||||
|
|
@ -139,17 +137,13 @@ class GoogleGmailConnector:
|
||||||
from app.utils.oauth_security import TokenEncryption
|
from app.utils.oauth_security import TokenEncryption
|
||||||
|
|
||||||
creds_dict = json.loads(self._credentials.to_json())
|
creds_dict = json.loads(self._credentials.to_json())
|
||||||
token_encrypted = connector.config.get(
|
token_encrypted = connector.config.get("_token_encrypted", False)
|
||||||
"_token_encrypted", False
|
|
||||||
)
|
|
||||||
|
|
||||||
if token_encrypted and config.SECRET_KEY:
|
if token_encrypted and config.SECRET_KEY:
|
||||||
token_encryption = TokenEncryption(config.SECRET_KEY)
|
token_encryption = TokenEncryption(config.SECRET_KEY)
|
||||||
if creds_dict.get("token"):
|
if creds_dict.get("token"):
|
||||||
creds_dict["token"] = (
|
creds_dict["token"] = token_encryption.encrypt_token(
|
||||||
token_encryption.encrypt_token(
|
creds_dict["token"]
|
||||||
creds_dict["token"]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if creds_dict.get("refresh_token"):
|
if creds_dict.get("refresh_token"):
|
||||||
creds_dict["refresh_token"] = (
|
creds_dict["refresh_token"] = (
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,9 @@ class ChucksHybridSearchRetriever:
|
||||||
|
|
||||||
# Add document type filter if provided (single string or list of strings)
|
# Add document type filter if provided (single string or list of strings)
|
||||||
if document_type is not None:
|
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 = []
|
doc_type_enums = []
|
||||||
for dt in type_list:
|
for dt in type_list:
|
||||||
if isinstance(dt, str):
|
if isinstance(dt, str):
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,9 @@ class DocumentHybridSearchRetriever:
|
||||||
|
|
||||||
# Add document type filter if provided (single string or list of strings)
|
# Add document type filter if provided (single string or list of strings)
|
||||||
if document_type is not None:
|
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 = []
|
doc_type_enums = []
|
||||||
for dt in type_list:
|
for dt in type_list:
|
||||||
if isinstance(dt, str):
|
if isinstance(dt, str):
|
||||||
|
|
|
||||||
|
|
@ -461,10 +461,14 @@ async def reauth_composio_connector(
|
||||||
return_url: Optional frontend path to redirect to after completion
|
return_url: Optional frontend path to redirect to after completion
|
||||||
"""
|
"""
|
||||||
if not ComposioService.is_enabled():
|
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:
|
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:
|
try:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
|
|
@ -502,7 +506,9 @@ async def reauth_composio_connector(
|
||||||
callback_base = config.COMPOSIO_REDIRECT_URI
|
callback_base = config.COMPOSIO_REDIRECT_URI
|
||||||
if not callback_base:
|
if not callback_base:
|
||||||
backend_url = config.BACKEND_URL or "http://localhost:8000"
|
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:
|
else:
|
||||||
# Replace the normal callback path with the reauth one
|
# Replace the normal callback path with the reauth one
|
||||||
callback_base = callback_base.replace(
|
callback_base = callback_base.replace(
|
||||||
|
|
@ -524,8 +530,13 @@ async def reauth_composio_connector(
|
||||||
connector.config = {**connector.config, "auth_expired": False}
|
connector.config = {**connector.config, "auth_expired": False}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(f"Composio account {connected_account_id} refreshed server-side (no redirect needed)")
|
logger.info(
|
||||||
return {"success": True, "message": "Authentication refreshed successfully."}
|
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}")
|
logger.info(f"Initiating Composio re-auth for connector {connector_id}")
|
||||||
return {"auth_url": refresh_result["redirect_url"]}
|
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)
|
credentials = build_composio_credentials(composio_connected_account_id)
|
||||||
drive_client = GoogleDriveClient(
|
drive_client = GoogleDriveClient(session, connector_id, credentials=credentials)
|
||||||
session, connector_id, credentials=credentials
|
|
||||||
)
|
|
||||||
|
|
||||||
items, error = await list_folder_contents(drive_client, parent_id=parent_id)
|
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}
|
connector.config = {**connector.config, "auth_expired": True}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
await session.commit()
|
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:
|
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(
|
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(
|
raise HTTPException(
|
||||||
status_code=500, detail=f"Failed to list folder contents: {error}"
|
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}
|
connector.config = {**connector.config, "auth_expired": True}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
await session.commit()
|
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:
|
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(
|
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
|
) from e
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail=f"Failed to list Drive contents: {e!s}"
|
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()
|
await session.commit()
|
||||||
logger.info(f"Marked connector {connector_id} as auth_expired")
|
logger.info(f"Marked connector {connector_id} as auth_expired")
|
||||||
except Exception:
|
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(
|
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(
|
raise HTTPException(
|
||||||
status_code=500, detail=f"Failed to list folder contents: {error}"
|
status_code=500, detail=f"Failed to list folder contents: {error}"
|
||||||
|
|
@ -562,9 +566,13 @@ async def list_google_drive_folders(
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(f"Marked connector {connector_id} as auth_expired")
|
logger.info(f"Marked connector {connector_id} as auth_expired")
|
||||||
except Exception:
|
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(
|
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
|
) from e
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail=f"Failed to list Drive contents: {e!s}"
|
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 = credentials.to_dict()
|
||||||
credentials_dict["_token_encrypted"] = True
|
credentials_dict["_token_encrypted"] = True
|
||||||
if connector.config.get("organization_name"):
|
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)
|
credentials_dict.pop("auth_expired", None)
|
||||||
connector.config = credentials_dict
|
connector.config = credentials_dict
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
|
|
|
||||||
|
|
@ -2374,7 +2374,11 @@ async def run_google_drive_indexing(
|
||||||
# Index each folder with indexing options
|
# Index each folder with indexing options
|
||||||
for folder in items.folders:
|
for folder in items.folders:
|
||||||
try:
|
try:
|
||||||
indexed_count, skipped_count, error_message = await index_google_drive_files(
|
(
|
||||||
|
indexed_count,
|
||||||
|
skipped_count,
|
||||||
|
error_message,
|
||||||
|
) = await index_google_drive_files(
|
||||||
session,
|
session,
|
||||||
connector_id,
|
connector_id,
|
||||||
search_space_id,
|
search_space_id,
|
||||||
|
|
@ -2429,7 +2433,9 @@ async def run_google_drive_indexing(
|
||||||
)
|
)
|
||||||
if _is_auth_error(error_message):
|
if _is_auth_error(error_message):
|
||||||
await _persist_auth_expired(session, connector_id)
|
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:
|
else:
|
||||||
# Update notification to storing stage
|
# Update notification to storing stage
|
||||||
if notification:
|
if notification:
|
||||||
|
|
|
||||||
|
|
@ -283,9 +283,7 @@ class ComposioService:
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
status = getattr(account, "status", "UNKNOWN")
|
status = getattr(account, "status", "UNKNOWN")
|
||||||
logger.info(
|
logger.info(f"Composio account {connected_account_id} is now {status}")
|
||||||
f"Composio account {connected_account_id} is now {status}"
|
|
||||||
)
|
|
||||||
return status
|
return status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,10 @@ class ConfluenceKBSyncService:
|
||||||
content_hash = unique_hash
|
content_hash = unique_hash
|
||||||
|
|
||||||
user_llm = await get_user_long_context_llm(
|
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 = {
|
doc_metadata_for_summary = {
|
||||||
|
|
@ -116,17 +119,26 @@ class ConfluenceKBSyncService:
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"KB sync after create succeeded: doc_id=%s, page=%s",
|
"KB sync after create succeeded: doc_id=%s, page=%s",
|
||||||
document.id, page_title,
|
document.id,
|
||||||
|
page_title,
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e).lower()
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": "Duplicate document detected"}
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
@ -215,11 +227,14 @@ class ConfluenceKBSyncService:
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"KB sync successful for document %s (%s)",
|
"KB sync successful for document %s (%s)",
|
||||||
document_id, page_title,
|
document_id,
|
||||||
|
page_title,
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,12 @@ class ConfluenceToolMetadataService:
|
||||||
for connector in connectors:
|
for connector in connectors:
|
||||||
auth_expired = await self._check_account_health(connector)
|
auth_expired = await self._check_account_health(connector)
|
||||||
workspace = ConfluenceWorkspace.from_connector(connector)
|
workspace = ConfluenceWorkspace.from_connector(connector)
|
||||||
accounts.append({
|
accounts.append(
|
||||||
**workspace.to_dict(),
|
{
|
||||||
"auth_expired": auth_expired,
|
**workspace.to_dict(),
|
||||||
})
|
"auth_expired": auth_expired,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if not auth_expired and not fetched_context:
|
if not auth_expired and not fetched_context:
|
||||||
try:
|
try:
|
||||||
|
|
@ -146,7 +148,8 @@ class ConfluenceToolMetadataService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to fetch Confluence spaces for connector %s: %s",
|
"Failed to fetch Confluence spaces for connector %s: %s",
|
||||||
connector.id, e,
|
connector.id,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -191,7 +194,11 @@ class ConfluenceToolMetadataService:
|
||||||
await client.close()
|
await client.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e).lower()
|
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 {
|
return {
|
||||||
"error": f"Failed to fetch Confluence page: {e!s}",
|
"error": f"Failed to fetch Confluence page: {e!s}",
|
||||||
"auth_expired": True,
|
"auth_expired": True,
|
||||||
|
|
@ -207,7 +214,9 @@ class ConfluenceToolMetadataService:
|
||||||
body_storage = storage.get("value", "")
|
body_storage = storage.get("value", "")
|
||||||
|
|
||||||
version_obj = page_data.get("version", {})
|
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 {
|
return {
|
||||||
"account": {**workspace.to_dict(), "auth_expired": False},
|
"account": {**workspace.to_dict(), "auth_expired": False},
|
||||||
|
|
@ -263,9 +272,7 @@ class ConfluenceToolMetadataService:
|
||||||
Document.document_type == DocumentType.CONFLUENCE_CONNECTOR,
|
Document.document_type == DocumentType.CONFLUENCE_CONNECTOR,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
or_(
|
or_(
|
||||||
func.lower(
|
func.lower(Document.document_metadata.op("->>")("page_title"))
|
||||||
Document.document_metadata.op("->>")("page_title")
|
|
||||||
)
|
|
||||||
== ref_lower,
|
== ref_lower,
|
||||||
func.lower(Document.title) == ref_lower,
|
func.lower(Document.title) == ref_lower,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -183,10 +183,12 @@ class GmailToolMetadataService:
|
||||||
and_(
|
and_(
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type.in_([
|
SearchSourceConnector.connector_type.in_(
|
||||||
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
[
|
||||||
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
|
SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||||
]),
|
SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
||||||
|
|
@ -223,9 +225,7 @@ class GmailToolMetadataService:
|
||||||
service = build("gmail", "v1", credentials=creds)
|
service = build("gmail", "v1", credentials=creds)
|
||||||
profile = await asyncio.get_event_loop().run_in_executor(
|
profile = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.users()
|
lambda: service.users().getProfile(userId="me").execute(),
|
||||||
.getProfile(userId="me")
|
|
||||||
.execute(),
|
|
||||||
)
|
)
|
||||||
acc_dict["email"] = profile.get("emailAddress", "")
|
acc_dict["email"] = profile.get("emailAddress", "")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -306,10 +306,12 @@ class GmailToolMetadataService:
|
||||||
|
|
||||||
draft = await asyncio.get_event_loop().run_in_executor(
|
draft = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.users()
|
lambda: (
|
||||||
.drafts()
|
service.users()
|
||||||
.get(userId="me", id=draft_id, format="full")
|
.drafts()
|
||||||
.execute(),
|
.get(userId="me", id=draft_id, format="full")
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
payload = draft.get("message", {}).get("payload", {})
|
payload = draft.get("message", {}).get("payload", {})
|
||||||
|
|
@ -422,15 +424,15 @@ class GmailToolMetadataService:
|
||||||
.filter(
|
.filter(
|
||||||
and_(
|
and_(
|
||||||
Document.search_space_id == search_space_id,
|
Document.search_space_id == search_space_id,
|
||||||
Document.document_type.in_([
|
Document.document_type.in_(
|
||||||
DocumentType.GOOGLE_GMAIL_CONNECTOR,
|
[
|
||||||
DocumentType.COMPOSIO_GMAIL_CONNECTOR,
|
DocumentType.GOOGLE_GMAIL_CONNECTOR,
|
||||||
]),
|
DocumentType.COMPOSIO_GMAIL_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
or_(
|
or_(
|
||||||
func.lower(
|
func.lower(cast(Document.document_metadata["subject"], String))
|
||||||
cast(Document.document_metadata["subject"], String)
|
|
||||||
)
|
|
||||||
== func.lower(email_ref),
|
== func.lower(email_ref),
|
||||||
func.lower(Document.title) == 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.future import select
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
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.services.llm_service import get_user_long_context_llm
|
||||||
from app.utils.document_converters import (
|
from app.utils.document_converters import (
|
||||||
create_document_chunks,
|
create_document_chunks,
|
||||||
|
|
@ -107,7 +112,9 @@ class GoogleCalendarKBSyncService:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("No LLM configured -- using fallback summary")
|
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)
|
summary_embedding = embed_text(summary_content)
|
||||||
|
|
||||||
chunks = await create_document_chunks(indexable_content)
|
chunks = await create_document_chunks(indexable_content)
|
||||||
|
|
@ -201,12 +208,16 @@ class GoogleCalendarKBSyncService:
|
||||||
None, lambda: build("calendar", "v3", credentials=creds)
|
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(
|
live_event = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.events()
|
lambda: (
|
||||||
.get(calendarId=calendar_id, eventId=event_id)
|
service.events()
|
||||||
.execute(),
|
.get(calendarId=calendar_id, eventId=event_id)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
event_summary = live_event.get("summary", "")
|
event_summary = live_event.get("summary", "")
|
||||||
|
|
@ -220,7 +231,10 @@ class GoogleCalendarKBSyncService:
|
||||||
end_time = end_data.get("dateTime", end_data.get("date", ""))
|
end_time = end_data.get("dateTime", end_data.get("date", ""))
|
||||||
|
|
||||||
attendees = [
|
attendees = [
|
||||||
{"email": a.get("email", ""), "responseStatus": a.get("responseStatus", "")}
|
{
|
||||||
|
"email": a.get("email", ""),
|
||||||
|
"responseStatus": a.get("responseStatus", ""),
|
||||||
|
}
|
||||||
for a in live_event.get("attendees", [])
|
for a in live_event.get("attendees", [])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -252,7 +266,9 @@ class GoogleCalendarKBSyncService:
|
||||||
indexable_content, user_llm, doc_metadata_for_summary
|
indexable_content, user_llm, doc_metadata_for_summary
|
||||||
)
|
)
|
||||||
else:
|
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)
|
summary_embedding = embed_text(summary_content)
|
||||||
|
|
||||||
chunks = await create_document_chunks(indexable_content)
|
chunks = await create_document_chunks(indexable_content)
|
||||||
|
|
@ -313,7 +329,10 @@ class GoogleCalendarKBSyncService:
|
||||||
if not connector:
|
if not connector:
|
||||||
raise ValueError(f"Connector {connector_id} not found")
|
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")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
if cca_id:
|
if cca_id:
|
||||||
return build_composio_credentials(cca_id)
|
return build_composio_credentials(cca_id)
|
||||||
|
|
@ -328,11 +347,17 @@ class GoogleCalendarKBSyncService:
|
||||||
if token_encrypted and app_config.SECRET_KEY:
|
if token_encrypted and app_config.SECRET_KEY:
|
||||||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||||
if config_data.get("token"):
|
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"):
|
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"):
|
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", "")
|
exp = config_data.get("expiry", "")
|
||||||
if exp:
|
if exp:
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ class GoogleCalendarAccount:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_connector(cls, connector: SearchSourceConnector) -> "GoogleCalendarAccount":
|
def from_connector(
|
||||||
|
cls, connector: SearchSourceConnector
|
||||||
|
) -> "GoogleCalendarAccount":
|
||||||
return cls(id=connector.id, name=connector.name)
|
return cls(id=connector.id, name=connector.name)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
|
@ -93,7 +95,10 @@ class GoogleCalendarToolMetadataService:
|
||||||
self._db_session = db_session
|
self._db_session = db_session
|
||||||
|
|
||||||
async def _build_credentials(self, connector: SearchSourceConnector) -> Credentials:
|
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")
|
cca_id = connector.config.get("composio_connected_account_id")
|
||||||
if cca_id:
|
if cca_id:
|
||||||
return build_composio_credentials(cca_id)
|
return build_composio_credentials(cca_id)
|
||||||
|
|
@ -108,11 +113,17 @@ class GoogleCalendarToolMetadataService:
|
||||||
if token_encrypted and app_config.SECRET_KEY:
|
if token_encrypted and app_config.SECRET_KEY:
|
||||||
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
token_encryption = TokenEncryption(app_config.SECRET_KEY)
|
||||||
if config_data.get("token"):
|
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"):
|
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"):
|
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", "")
|
exp = config_data.get("expiry", "")
|
||||||
if exp:
|
if exp:
|
||||||
|
|
@ -149,10 +160,12 @@ class GoogleCalendarToolMetadataService:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(
|
await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: build("calendar", "v3", credentials=creds)
|
lambda: (
|
||||||
.calendarList()
|
build("calendar", "v3", credentials=creds)
|
||||||
.list(maxResults=1)
|
.calendarList()
|
||||||
.execute(),
|
.list(maxResults=1)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -252,11 +265,13 @@ class GoogleCalendarToolMetadataService:
|
||||||
None, lambda: service.calendarList().list().execute()
|
None, lambda: service.calendarList().list().execute()
|
||||||
)
|
)
|
||||||
for cal in cal_list.get("items", []):
|
for cal in cal_list.get("items", []):
|
||||||
calendars.append({
|
calendars.append(
|
||||||
"id": cal.get("id", ""),
|
{
|
||||||
"summary": cal.get("summary", ""),
|
"id": cal.get("id", ""),
|
||||||
"primary": cal.get("primary", False),
|
"summary": cal.get("summary", ""),
|
||||||
})
|
"primary": cal.get("primary", False),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
tz_setting = await loop.run_in_executor(
|
tz_setting = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
|
|
@ -314,23 +329,34 @@ class GoogleCalendarToolMetadataService:
|
||||||
calendar_id = event.calendar_id or "primary"
|
calendar_id = event.calendar_id or "primary"
|
||||||
live_event = await loop.run_in_executor(
|
live_event = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
lambda: service.events()
|
lambda: (
|
||||||
.get(calendarId=calendar_id, eventId=event.event_id)
|
service.events()
|
||||||
.execute(),
|
.get(calendarId=calendar_id, eventId=event.event_id)
|
||||||
|
.execute()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
event_dict["summary"] = live_event.get("summary", event_dict["summary"])
|
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"])
|
event_dict["location"] = live_event.get("location", event_dict["location"])
|
||||||
|
|
||||||
start_data = live_event.get("start", {})
|
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", {})
|
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"] = [
|
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", [])
|
for a in live_event.get("attendees", [])
|
||||||
]
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ class GoogleDriveKBSyncService:
|
||||||
|
|
||||||
indexable_content = (content or "").strip()
|
indexable_content = (content or "").strip()
|
||||||
if not indexable_content:
|
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)
|
content_hash = generate_content_hash(indexable_content, search_space_id)
|
||||||
|
|
||||||
|
|
@ -93,7 +95,9 @@ class GoogleDriveKBSyncService:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("No LLM configured — using fallback summary")
|
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)
|
summary_embedding = embed_text(summary_content)
|
||||||
|
|
||||||
chunks = await create_document_chunks(indexable_content)
|
chunks = await create_document_chunks(indexable_content)
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,12 @@ class GoogleDriveToolMetadataService:
|
||||||
and_(
|
and_(
|
||||||
SearchSourceConnector.id == document.connector_id,
|
SearchSourceConnector.id == document.connector_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type.in_([
|
SearchSourceConnector.connector_type.in_(
|
||||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
[
|
||||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||||
]),
|
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -168,10 +170,12 @@ class GoogleDriveToolMetadataService:
|
||||||
and_(
|
and_(
|
||||||
SearchSourceConnector.search_space_id == search_space_id,
|
SearchSourceConnector.search_space_id == search_space_id,
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
SearchSourceConnector.connector_type.in_([
|
SearchSourceConnector.connector_type.in_(
|
||||||
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
[
|
||||||
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR,
|
||||||
]),
|
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
.order_by(SearchSourceConnector.last_indexed_at.desc())
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ class JiraKBSyncService:
|
||||||
if existing:
|
if existing:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Document for Jira issue %s already exists (doc_id=%s), skipping",
|
"Document for Jira issue %s already exists (doc_id=%s), skipping",
|
||||||
issue_identifier, existing.id,
|
issue_identifier,
|
||||||
|
existing.id,
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
|
|
@ -61,7 +62,9 @@ class JiraKBSyncService:
|
||||||
if not indexable_content:
|
if not indexable_content:
|
||||||
indexable_content = f"Jira Issue {issue_identifier}: {issue_title}"
|
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)
|
content_hash = generate_content_hash(issue_content, search_space_id)
|
||||||
|
|
||||||
|
|
@ -73,7 +76,10 @@ class JiraKBSyncService:
|
||||||
content_hash = unique_hash
|
content_hash = unique_hash
|
||||||
|
|
||||||
user_llm = await get_user_long_context_llm(
|
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 = {
|
doc_metadata_for_summary = {
|
||||||
|
|
@ -88,7 +94,9 @@ class JiraKBSyncService:
|
||||||
issue_content, user_llm, doc_metadata_for_summary
|
issue_content, user_llm, doc_metadata_for_summary
|
||||||
)
|
)
|
||||||
else:
|
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)
|
summary_embedding = embed_text(summary_content)
|
||||||
|
|
||||||
chunks = await create_document_chunks(issue_content)
|
chunks = await create_document_chunks(issue_content)
|
||||||
|
|
@ -122,17 +130,26 @@ class JiraKBSyncService:
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"KB sync after create succeeded: doc_id=%s, issue=%s",
|
"KB sync after create succeeded: doc_id=%s, issue=%s",
|
||||||
document.id, issue_identifier,
|
document.id,
|
||||||
|
issue_identifier,
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e).lower()
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": "Duplicate document detected"}
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
@ -189,14 +206,18 @@ class JiraKBSyncService:
|
||||||
issue_content, user_llm, doc_meta
|
issue_content, user_llm, doc_meta
|
||||||
)
|
)
|
||||||
else:
|
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)
|
summary_embedding = embed_text(summary_content)
|
||||||
|
|
||||||
chunks = await create_document_chunks(issue_content)
|
chunks = await create_document_chunks(issue_content)
|
||||||
|
|
||||||
document.title = f"{issue_identifier}: {issue_title}"
|
document.title = f"{issue_identifier}: {issue_title}"
|
||||||
document.content = summary_content
|
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
|
document.embedding = summary_embedding
|
||||||
|
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
@ -219,11 +240,15 @@ class JiraKBSyncService:
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"KB sync successful for document %s (%s: %s)",
|
"KB sync successful for document %s (%s: %s)",
|
||||||
document_id, issue_identifier, issue_title,
|
document_id,
|
||||||
|
issue_identifier,
|
||||||
|
issue_title,
|
||||||
)
|
)
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
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()
|
await self.db_session.rollback()
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,7 @@ class JiraToolMetadataService:
|
||||||
await asyncio.to_thread(jira_client.get_myself)
|
await asyncio.to_thread(jira_client.get_myself)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning("Jira connector %s health check failed: %s", connector.id, e)
|
||||||
"Jira connector %s health check failed: %s", connector.id, e
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
connector.config = {**connector.config, "auth_expired": True}
|
connector.config = {**connector.config, "auth_expired": True}
|
||||||
flag_modified(connector, "config")
|
flag_modified(connector, "config")
|
||||||
|
|
@ -165,7 +163,8 @@ class JiraToolMetadataService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to fetch Jira context for connector %s: %s",
|
"Failed to fetch Jira context for connector %s: %s",
|
||||||
connector.id, e,
|
connector.id,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -209,13 +208,15 @@ class JiraToolMetadataService:
|
||||||
session=self._db_session, connector_id=connector.id
|
session=self._db_session, connector_id=connector.id
|
||||||
)
|
)
|
||||||
jira_client = await jira_history._get_jira_client()
|
jira_client = await jira_history._get_jira_client()
|
||||||
issue_data = await asyncio.to_thread(
|
issue_data = await asyncio.to_thread(jira_client.get_issue, issue.issue_id)
|
||||||
jira_client.get_issue, issue.issue_id
|
|
||||||
)
|
|
||||||
formatted = jira_client.format_issue(issue_data)
|
formatted = jira_client.format_issue(issue_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e).lower()
|
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 {
|
return {
|
||||||
"error": f"Failed to fetch Jira issue: {e!s}",
|
"error": f"Failed to fetch Jira issue: {e!s}",
|
||||||
"auth_expired": True,
|
"auth_expired": True,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,9 @@ class LinearKBSyncService:
|
||||||
if not indexable_content:
|
if not indexable_content:
|
||||||
indexable_content = f"Linear Issue {issue_identifier}: {issue_title}"
|
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)
|
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)
|
issue_api = await self._fetch_issue_context(linear_client, issue.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e).lower()
|
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 {
|
return {
|
||||||
"error": f"Failed to fetch Linear issue context: {e!s}",
|
"error": f"Failed to fetch Linear issue context: {e!s}",
|
||||||
"auth_expired": True,
|
"auth_expired": True,
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,10 @@ class NotionToolMetadataService:
|
||||||
)
|
)
|
||||||
db_connector = result.scalar_one_or_none()
|
db_connector = result.scalar_one_or_none()
|
||||||
if db_connector and not db_connector.config.get("auth_expired"):
|
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")
|
flag_modified(db_connector, "config")
|
||||||
await self._db_session.commit()
|
await self._db_session.commit()
|
||||||
await self._db_session.refresh(db_connector)
|
await self._db_session.refresh(db_connector)
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,7 @@ async def index_google_calendar_events(
|
||||||
|
|
||||||
# Build credentials based on connector type
|
# Build credentials based on connector type
|
||||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||||
connected_account_id = connector.config.get(
|
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||||
"composio_connected_account_id"
|
|
||||||
)
|
|
||||||
if not connected_account_id:
|
if not connected_account_id:
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry,
|
log_entry,
|
||||||
|
|
@ -396,10 +394,19 @@ async def index_google_calendar_events(
|
||||||
session, legacy_hash
|
session, legacy_hash
|
||||||
)
|
)
|
||||||
if existing_document:
|
if existing_document:
|
||||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
existing_document.unique_identifier_hash = (
|
||||||
if existing_document.document_type == DocumentType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR:
|
unique_identifier_hash
|
||||||
existing_document.document_type = DocumentType.GOOGLE_CALENDAR_CONNECTOR
|
)
|
||||||
logger.info(f"Migrated legacy Composio Calendar document: {event_id}")
|
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:
|
if existing_document:
|
||||||
# Document exists - check if content has changed
|
# Document exists - check if content has changed
|
||||||
|
|
|
||||||
|
|
@ -121,13 +121,13 @@ async def index_google_drive_files(
|
||||||
# Build credentials based on connector type
|
# Build credentials based on connector type
|
||||||
pre_built_credentials = None
|
pre_built_credentials = None
|
||||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||||
connected_account_id = connector.config.get(
|
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||||
"composio_connected_account_id"
|
|
||||||
)
|
|
||||||
if not connected_account_id:
|
if not connected_account_id:
|
||||||
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry, error_msg, "Missing Composio account",
|
log_entry,
|
||||||
|
error_msg,
|
||||||
|
"Missing Composio account",
|
||||||
{"error_type": "MissingComposioAccount"},
|
{"error_type": "MissingComposioAccount"},
|
||||||
)
|
)
|
||||||
return 0, 0, error_msg
|
return 0, 0, error_msg
|
||||||
|
|
@ -355,13 +355,13 @@ async def index_google_drive_single_file(
|
||||||
|
|
||||||
pre_built_credentials = None
|
pre_built_credentials = None
|
||||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||||
connected_account_id = connector.config.get(
|
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||||
"composio_connected_account_id"
|
|
||||||
)
|
|
||||||
if not connected_account_id:
|
if not connected_account_id:
|
||||||
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
error_msg = f"Composio connected_account_id not found for connector {connector_id}"
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry, error_msg, "Missing Composio account",
|
log_entry,
|
||||||
|
error_msg,
|
||||||
|
"Missing Composio account",
|
||||||
{"error_type": "MissingComposioAccount"},
|
{"error_type": "MissingComposioAccount"},
|
||||||
)
|
)
|
||||||
return 0, error_msg
|
return 0, error_msg
|
||||||
|
|
@ -611,7 +611,11 @@ async def _index_full_scan(
|
||||||
|
|
||||||
if not files_to_process and first_listing_error:
|
if not files_to_process and first_listing_error:
|
||||||
error_lower = first_listing_error.lower()
|
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(
|
raise Exception(
|
||||||
f"Google Drive authentication failed. Please re-authenticate. "
|
f"Google Drive authentication failed. Please re-authenticate. "
|
||||||
f"(Error: {first_listing_error})"
|
f"(Error: {first_listing_error})"
|
||||||
|
|
@ -704,7 +708,11 @@ async def _index_with_delta_sync(
|
||||||
if error:
|
if error:
|
||||||
logger.error(f"Error fetching changes: {error}")
|
logger.error(f"Error fetching changes: {error}")
|
||||||
error_lower = error.lower()
|
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(
|
raise Exception(
|
||||||
f"Google Drive authentication failed. Please re-authenticate. "
|
f"Google Drive authentication failed. Please re-authenticate. "
|
||||||
f"(Error: {error})"
|
f"(Error: {error})"
|
||||||
|
|
@ -872,7 +880,10 @@ async def _create_pending_document_for_file(
|
||||||
)
|
)
|
||||||
if existing_document:
|
if existing_document:
|
||||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
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
|
existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE
|
||||||
logger.info(f"Migrated legacy Composio document to native type: {file_id}")
|
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(
|
result = await session.execute(
|
||||||
select(Document).where(
|
select(Document).where(
|
||||||
Document.search_space_id == search_space_id,
|
Document.search_space_id == search_space_id,
|
||||||
Document.document_type.in_([
|
Document.document_type.in_(
|
||||||
DocumentType.GOOGLE_DRIVE_FILE,
|
[
|
||||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
DocumentType.GOOGLE_DRIVE_FILE,
|
||||||
]),
|
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
cast(Document.document_metadata["google_drive_file_id"], String)
|
cast(Document.document_metadata["google_drive_file_id"], String)
|
||||||
== file_id,
|
== file_id,
|
||||||
)
|
)
|
||||||
|
|
@ -1000,7 +1013,10 @@ async def _check_rename_only_update(
|
||||||
if existing_document:
|
if existing_document:
|
||||||
if existing_document.unique_identifier_hash != primary_hash:
|
if existing_document.unique_identifier_hash != primary_hash:
|
||||||
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
|
existing_document.document_type = DocumentType.GOOGLE_DRIVE_FILE
|
||||||
logger.info(f"Migrated legacy Composio Drive document: {file_id}")
|
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(
|
result = await session.execute(
|
||||||
select(Document).where(
|
select(Document).where(
|
||||||
Document.search_space_id == search_space_id,
|
Document.search_space_id == search_space_id,
|
||||||
Document.document_type.in_([
|
Document.document_type.in_(
|
||||||
DocumentType.GOOGLE_DRIVE_FILE,
|
[
|
||||||
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
DocumentType.GOOGLE_DRIVE_FILE,
|
||||||
]),
|
DocumentType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||||
|
]
|
||||||
|
),
|
||||||
cast(Document.document_metadata["google_drive_file_id"], String)
|
cast(Document.document_metadata["google_drive_file_id"], String)
|
||||||
== file_id,
|
== file_id,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,7 @@ async def index_google_gmail_messages(
|
||||||
|
|
||||||
# Build credentials based on connector type
|
# Build credentials based on connector type
|
||||||
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
if connector.connector_type in COMPOSIO_GOOGLE_CONNECTOR_TYPES:
|
||||||
connected_account_id = connector.config.get(
|
connected_account_id = connector.config.get("composio_connected_account_id")
|
||||||
"composio_connected_account_id"
|
|
||||||
)
|
|
||||||
if not connected_account_id:
|
if not connected_account_id:
|
||||||
await task_logger.log_task_failure(
|
await task_logger.log_task_failure(
|
||||||
log_entry,
|
log_entry,
|
||||||
|
|
@ -323,10 +321,19 @@ async def index_google_gmail_messages(
|
||||||
session, legacy_hash
|
session, legacy_hash
|
||||||
)
|
)
|
||||||
if existing_document:
|
if existing_document:
|
||||||
existing_document.unique_identifier_hash = unique_identifier_hash
|
existing_document.unique_identifier_hash = (
|
||||||
if existing_document.document_type == DocumentType.COMPOSIO_GMAIL_CONNECTOR:
|
unique_identifier_hash
|
||||||
existing_document.document_type = DocumentType.GOOGLE_GMAIL_CONNECTOR
|
)
|
||||||
logger.info(f"Migrated legacy Composio Gmail document: {message_id}")
|
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:
|
if existing_document:
|
||||||
# Document exists - check if content has changed
|
# Document exists - check if content has changed
|
||||||
|
|
|
||||||
|
|
@ -1270,9 +1270,16 @@ async def process_file_in_background(
|
||||||
print("Error deleting temp file", e)
|
print("Error deleting temp file", e)
|
||||||
pass
|
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(
|
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,
|
enable_summary=enable_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1414,7 +1421,9 @@ async def process_file_in_background(
|
||||||
# Extract text content from the markdown documents
|
# Extract text content from the markdown documents
|
||||||
markdown_content = doc.text
|
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(
|
doc_result = await add_received_file_document_using_llamacloud(
|
||||||
session,
|
session,
|
||||||
filename,
|
filename,
|
||||||
|
|
@ -1569,7 +1578,9 @@ async def process_file_in_background(
|
||||||
session, notification, stage="chunking"
|
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(
|
doc_result = await add_received_file_document_using_docling(
|
||||||
session,
|
session,
|
||||||
filename,
|
filename,
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,7 @@ async def committed_google_data(async_engine):
|
||||||
session.add(user)
|
session.add(user)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
space = SearchSpace(
|
space = SearchSpace(name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id)
|
||||||
name=f"Google Test {uuid.uuid4().hex[:6]}", user_id=user.id
|
|
||||||
)
|
|
||||||
session.add(space)
|
session.add(space)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
space_id = space.id
|
space_id = space.id
|
||||||
|
|
@ -215,7 +213,9 @@ async def committed_google_data(async_engine):
|
||||||
def patched_session_factory(async_engine, monkeypatch):
|
def patched_session_factory(async_engine, monkeypatch):
|
||||||
"""Replace ``async_session_maker`` in connector_service with one bound to the test engine."""
|
"""Replace ``async_session_maker`` in connector_service with one bound to the test engine."""
|
||||||
test_maker = async_sessionmaker(async_engine, expire_on_commit=False)
|
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
|
return test_maker
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
||||||
|
|
||||||
from app.db import SearchSourceConnectorType
|
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
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
@ -52,8 +57,10 @@ async def native_calendar(async_engine):
|
||||||
async_engine,
|
async_engine,
|
||||||
connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
|
connector_type=SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR,
|
||||||
config={
|
config={
|
||||||
"token": "fake", "refresh_token": "fake",
|
"token": "fake",
|
||||||
"client_id": "fake", "client_secret": "fake",
|
"refresh_token": "fake",
|
||||||
|
"client_id": "fake",
|
||||||
|
"client_secret": "fake",
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
},
|
},
|
||||||
name_prefix="cal-native",
|
name_prefix="cal-native",
|
||||||
|
|
@ -66,10 +73,16 @@ async def native_calendar(async_engine):
|
||||||
@patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector")
|
@patch(f"{_INDEXER_MODULE}.GoogleCalendarConnector")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_composio_calendar_uses_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."""
|
"""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
|
data = composio_calendar
|
||||||
mock_creds = MagicMock(name="composio-creds")
|
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_tl_cls.return_value = mock_task_logger()
|
||||||
|
|
||||||
mock_cal_instance = MagicMock()
|
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
|
mock_cal_cls.return_value = mock_cal_instance
|
||||||
|
|
||||||
maker = make_session_factory(async_engine)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
await index_google_calendar_events(
|
await index_google_calendar_events(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
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)
|
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}.TaskLoggingService")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_composio_calendar_without_account_id_returns_error(
|
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."""
|
"""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
|
data = composio_calendar_no_id
|
||||||
mock_tl_cls.return_value = mock_task_logger()
|
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)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
count, _skipped, error = await index_google_calendar_events(
|
count, _skipped, error = await index_google_calendar_events(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
connector_id=data["connector_id"],
|
||||||
|
search_space_id=data["search_space_id"],
|
||||||
|
user_id=data["user_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert count == 0
|
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}.GoogleCalendarConnector")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_native_calendar_does_not_use_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."""
|
"""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
|
data = native_calendar
|
||||||
mock_tl_cls.return_value = mock_task_logger()
|
mock_tl_cls.return_value = mock_task_logger()
|
||||||
|
|
||||||
mock_cal_instance = MagicMock()
|
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
|
mock_cal_cls.return_value = mock_cal_instance
|
||||||
|
|
||||||
maker = make_session_factory(async_engine)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
await index_google_calendar_events(
|
await index_google_calendar_events(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
connector_id=data["connector_id"],
|
||||||
|
search_space_id=data["search_space_id"],
|
||||||
|
user_id=data["user_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_build_creds.assert_not_called()
|
mock_build_creds.assert_not_called()
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
||||||
|
|
||||||
from app.db import SearchSourceConnectorType
|
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
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
@ -129,7 +134,9 @@ async def test_composio_connector_without_account_id_returns_error(
|
||||||
|
|
||||||
assert count == 0
|
assert count == 0
|
||||||
assert error is not None
|
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()
|
mock_build_creds.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,12 @@ import pytest_asyncio
|
||||||
|
|
||||||
from app.db import SearchSourceConnectorType
|
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
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
@ -52,8 +57,10 @@ async def native_gmail(async_engine):
|
||||||
async_engine,
|
async_engine,
|
||||||
connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
connector_type=SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR,
|
||||||
config={
|
config={
|
||||||
"token": "fake", "refresh_token": "fake",
|
"token": "fake",
|
||||||
"client_id": "fake", "client_secret": "fake",
|
"refresh_token": "fake",
|
||||||
|
"client_id": "fake",
|
||||||
|
"client_secret": "fake",
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
},
|
},
|
||||||
name_prefix="gmail-native",
|
name_prefix="gmail-native",
|
||||||
|
|
@ -66,10 +73,16 @@ async def native_gmail(async_engine):
|
||||||
@patch(f"{_INDEXER_MODULE}.GoogleGmailConnector")
|
@patch(f"{_INDEXER_MODULE}.GoogleGmailConnector")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_composio_gmail_uses_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."""
|
"""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
|
data = composio_gmail
|
||||||
mock_creds = MagicMock(name="composio-creds")
|
mock_creds = MagicMock(name="composio-creds")
|
||||||
|
|
@ -83,8 +96,10 @@ async def test_composio_gmail_uses_composio_credentials(
|
||||||
maker = make_session_factory(async_engine)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
await index_google_gmail_messages(
|
await index_google_gmail_messages(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
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)
|
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}.TaskLoggingService")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_composio_gmail_without_account_id_returns_error(
|
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."""
|
"""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
|
data = composio_gmail_no_id
|
||||||
mock_tl_cls.return_value = mock_task_logger()
|
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)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
count, _skipped, error = await index_google_gmail_messages(
|
count, _skipped, error = await index_google_gmail_messages(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
connector_id=data["connector_id"],
|
||||||
|
search_space_id=data["search_space_id"],
|
||||||
|
user_id=data["user_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert count == 0
|
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}.GoogleGmailConnector")
|
||||||
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
@patch(f"{_INDEXER_MODULE}.build_composio_credentials")
|
||||||
async def test_native_gmail_does_not_use_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."""
|
"""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
|
data = native_gmail
|
||||||
mock_tl_cls.return_value = mock_task_logger()
|
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)
|
maker = make_session_factory(async_engine)
|
||||||
async with maker() as session:
|
async with maker() as session:
|
||||||
await index_google_gmail_messages(
|
await index_google_gmail_messages(
|
||||||
session=session, connector_id=data["connector_id"],
|
session=session,
|
||||||
search_space_id=data["search_space_id"], user_id=data["user_id"],
|
connector_id=data["connector_id"],
|
||||||
|
search_space_id=data["search_space_id"],
|
||||||
|
user_id=data["user_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_build_creds.assert_not_called()
|
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
|
assert "FILE" not in returned_types
|
||||||
|
|
||||||
|
|
||||||
async def test_single_string_type_returns_only_that_type(
|
async def test_single_string_type_returns_only_that_type(db_session, seed_google_docs):
|
||||||
db_session, seed_google_docs
|
|
||||||
):
|
|
||||||
"""Searching with a single string type returns only documents of that exact type."""
|
"""Searching with a single string type returns only documents of that exact type."""
|
||||||
space_id = seed_google_docs["search_space"].id
|
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
|
mock_build.return_value = mock_service
|
||||||
|
|
||||||
connector = GoogleGmailConnector(
|
connector = GoogleGmailConnector(
|
||||||
creds, session=MagicMock(), user_id="test-user",
|
creds,
|
||||||
|
session=MagicMock(),
|
||||||
|
user_id="test-user",
|
||||||
)
|
)
|
||||||
|
|
||||||
profile, error = await connector.get_user_profile()
|
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.Request")
|
||||||
@patch("app.connectors.google_gmail_connector.build")
|
@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
|
"""GoogleGmailConnector handles expired Composio credentials via refresh_handler
|
||||||
without attempting DB persistence."""
|
without attempting DB persistence."""
|
||||||
from app.connectors.google_gmail_connector import GoogleGmailConnector
|
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()
|
mock_session = AsyncMock()
|
||||||
connector = GoogleGmailConnector(
|
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()
|
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
|
mock_build.return_value = mock_service
|
||||||
|
|
||||||
connector = GoogleCalendarConnector(
|
connector = GoogleCalendarConnector(
|
||||||
creds, session=MagicMock(), user_id="test-user",
|
creds,
|
||||||
|
session=MagicMock(),
|
||||||
|
user_id="test-user",
|
||||||
)
|
)
|
||||||
|
|
||||||
calendars, error = await connector.get_calendars()
|
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.Request")
|
||||||
@patch("app.connectors.google_calendar_connector.build")
|
@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
|
"""GoogleCalendarConnector handles expired Composio credentials via refresh_handler
|
||||||
without attempting DB persistence."""
|
without attempting DB persistence."""
|
||||||
from app.connectors.google_calendar_connector import GoogleCalendarConnector
|
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()
|
mock_session = AsyncMock()
|
||||||
connector = GoogleCalendarConnector(
|
connector = GoogleCalendarConnector(
|
||||||
creds, session=mock_session, user_id="test-user",
|
creds,
|
||||||
|
session=mock_session,
|
||||||
|
user_id="test-user",
|
||||||
)
|
)
|
||||||
|
|
||||||
calendars, error = await connector.get_calendars()
|
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
|
mock_build.return_value = mock_service
|
||||||
|
|
||||||
client = GoogleDriveClient(
|
client = GoogleDriveClient(
|
||||||
session=MagicMock(), connector_id=999, credentials=creds,
|
session=MagicMock(),
|
||||||
|
connector_id=999,
|
||||||
|
credentials=creds,
|
||||||
)
|
)
|
||||||
|
|
||||||
files, next_token, error = await client.list_files()
|
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
|
mock_build.return_value = mock_service
|
||||||
|
|
||||||
client = GoogleDriveClient(
|
client = GoogleDriveClient(
|
||||||
session=MagicMock(), connector_id=999, credentials=creds,
|
session=MagicMock(),
|
||||||
|
connector_id=999,
|
||||||
|
credentials=creds,
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.list_files()
|
await client.list_files()
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,14 @@ def test_drive_indexer_accepts_both_native_and_composio():
|
||||||
ACCEPTED_DRIVE_CONNECTOR_TYPES,
|
ACCEPTED_DRIVE_CONNECTOR_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert SearchSourceConnectorType.GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
assert (
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in ACCEPTED_DRIVE_CONNECTOR_TYPES
|
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():
|
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,
|
ACCEPTED_GMAIL_CONNECTOR_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
assert (
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in ACCEPTED_GMAIL_CONNECTOR_TYPES
|
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():
|
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,
|
ACCEPTED_CALENDAR_CONNECTOR_TYPES,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert SearchSourceConnectorType.GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
assert (
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in ACCEPTED_CALENDAR_CONNECTOR_TYPES
|
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():
|
def test_composio_connector_types_set_covers_all_google_services():
|
||||||
"""COMPOSIO_GOOGLE_CONNECTOR_TYPES should contain all three Composio Google types."""
|
"""COMPOSIO_GOOGLE_CONNECTOR_TYPES should contain all three Composio Google types."""
|
||||||
from app.utils.google_credentials import COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
from app.utils.google_credentials import COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
||||||
|
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
assert (
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GMAIL_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
SearchSourceConnectorType.COMPOSIO_GOOGLE_DRIVE_CONNECTOR
|
||||||
assert SearchSourceConnectorType.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR in COMPOSIO_GOOGLE_CONNECTOR_TYPES
|
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}
|
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"
|
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 ? (
|
{isLoggingIn ? <Spinner size="sm" className="text-white" /> : t("sign_in")}
|
||||||
<Spinner size="sm" className="text-white" />
|
|
||||||
) : (
|
|
||||||
t("sign_in")
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ export async function GET(
|
||||||
connectorId: searchParams.get("connectorId"),
|
connectorId: searchParams.get("connectorId"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirectUrl = new URL(
|
const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url);
|
||||||
`/dashboard/${search_space_id}/new-chat`,
|
|
||||||
request.url
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = NextResponse.redirect(redirectUrl, { status: 302 });
|
const response = NextResponse.redirect(redirectUrl, { status: 302 });
|
||||||
response.cookies.set(OAUTH_RESULT_COOKIE, result, {
|
response.cookies.set(OAUTH_RESULT_COOKIE, result, {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
|
@ -16,12 +17,11 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
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 { 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 { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||||
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||||
|
|
@ -35,14 +35,9 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
|
|
@ -51,7 +46,12 @@ import {
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
} from "@/components/ui/drawer";
|
} 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 { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,26 @@ import { Thread } from "@/components/assistant-ui/thread";
|
||||||
import { MobileEditorPanel } from "@/components/editor-panel/editor-panel";
|
import { MobileEditorPanel } from "@/components/editor-panel/editor-panel";
|
||||||
import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel";
|
import { MobileHitlEditPanel } from "@/components/hitl-edit-panel/hitl-edit-panel";
|
||||||
import { MobileReportPanel } from "@/components/report-panel/report-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 type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||||
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
import { DisplayImageToolUI } from "@/components/tool-ui/display-image";
|
||||||
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
||||||
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
|
import { GenerateReportToolUI } from "@/components/tool-ui/generate-report";
|
||||||
import {
|
|
||||||
CreateCalendarEventToolUI,
|
|
||||||
UpdateCalendarEventToolUI,
|
|
||||||
DeleteCalendarEventToolUI,
|
|
||||||
} from "@/components/tool-ui/google-calendar";
|
|
||||||
import {
|
import {
|
||||||
CreateGmailDraftToolUI,
|
CreateGmailDraftToolUI,
|
||||||
SendGmailEmailToolUI,
|
SendGmailEmailToolUI,
|
||||||
TrashGmailEmailToolUI,
|
TrashGmailEmailToolUI,
|
||||||
UpdateGmailDraftToolUI,
|
UpdateGmailDraftToolUI,
|
||||||
} from "@/components/tool-ui/gmail";
|
} from "@/components/tool-ui/gmail";
|
||||||
|
import {
|
||||||
|
CreateCalendarEventToolUI,
|
||||||
|
DeleteCalendarEventToolUI,
|
||||||
|
UpdateCalendarEventToolUI,
|
||||||
|
} from "@/components/tool-ui/google-calendar";
|
||||||
import {
|
import {
|
||||||
CreateGoogleDriveFileToolUI,
|
CreateGoogleDriveFileToolUI,
|
||||||
DeleteGoogleDriveFileToolUI,
|
DeleteGoogleDriveFileToolUI,
|
||||||
|
|
@ -61,11 +66,6 @@ import {
|
||||||
DeleteJiraIssueToolUI,
|
DeleteJiraIssueToolUI,
|
||||||
UpdateJiraIssueToolUI,
|
UpdateJiraIssueToolUI,
|
||||||
} from "@/components/tool-ui/jira";
|
} from "@/components/tool-ui/jira";
|
||||||
import {
|
|
||||||
CreateConfluencePageToolUI,
|
|
||||||
DeleteConfluencePageToolUI,
|
|
||||||
UpdateConfluencePageToolUI,
|
|
||||||
} from "@/components/tool-ui/confluence";
|
|
||||||
import {
|
import {
|
||||||
CreateLinearIssueToolUI,
|
CreateLinearIssueToolUI,
|
||||||
DeleteLinearIssueToolUI,
|
DeleteLinearIssueToolUI,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,6 +43,7 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import posthog from "posthog-js";
|
|
||||||
import NextError from "next/error";
|
import NextError from "next/error";
|
||||||
|
import posthog from "posthog-js";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default function GlobalError({
|
export default function GlobalError({
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ interface HitlEditPanelState {
|
||||||
content: string;
|
content: string;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
extraFields?: ExtraField[];
|
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;
|
onClose: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -214,11 +214,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
if (!searchSpaceId) return null;
|
if (!searchSpaceId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
|
||||||
open={isOpen}
|
|
||||||
modal={false}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
>
|
|
||||||
{showTrigger && (
|
{showTrigger && (
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
data-joyride="connector-icon"
|
data-joyride="connector-icon"
|
||||||
|
|
@ -354,11 +350,12 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
onBack={handleBackFromEdit}
|
onBack={handleBackFromEdit}
|
||||||
onQuickIndex={(() => {
|
onQuickIndex={(() => {
|
||||||
const cfg = connectorConfig || editingConnector.config;
|
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";
|
editingConnector.connector_type === "COMPOSIO_GOOGLE_DRIVE_CONNECTOR";
|
||||||
const hasDriveItems = isDrive
|
const hasDriveItems = isDrive
|
||||||
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
? ((cfg?.selected_folders as unknown[]) ?? []).length > 0 ||
|
||||||
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
((cfg?.selected_files as unknown[]) ?? []).length > 0
|
||||||
: true;
|
: true;
|
||||||
if (!hasDriveItems) return undefined;
|
if (!hasDriveItems) return undefined;
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -376,37 +373,37 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
||||||
onNameChange={setConnectorName}
|
onNameChange={setConnectorName}
|
||||||
/>
|
/>
|
||||||
) : indexingConfig ? (
|
) : indexingConfig ? (
|
||||||
<IndexingConfigurationView
|
<IndexingConfigurationView
|
||||||
config={indexingConfig}
|
config={indexingConfig}
|
||||||
connector={
|
connector={
|
||||||
indexingConnector
|
indexingConnector
|
||||||
? {
|
? {
|
||||||
...indexingConnector,
|
...indexingConnector,
|
||||||
config: indexingConnectorConfig || indexingConnector.config,
|
config: indexingConnectorConfig || indexingConnector.config,
|
||||||
}
|
}
|
||||||
: undefined
|
: 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);
|
|
||||||
}
|
}
|
||||||
handleStartIndexing(() => refreshConnectors());
|
startDate={startDate}
|
||||||
}}
|
endDate={endDate}
|
||||||
onSkip={handleSkipIndexing}
|
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
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useId, useRef, useState } from "react";
|
import { useId, useRef, useState } from "react";
|
||||||
|
|
@ -26,6 +25,7 @@ import {
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,7 @@ function getFileIconFromName(fileName: string, className: string = "size-3.5 shr
|
||||||
return <File className={`${className} text-gray-500`} />;
|
return <File className={`${className} text-gray-500`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
|
export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
|
||||||
connector,
|
|
||||||
onConfigChange,
|
|
||||||
}) => {
|
|
||||||
const isIndexable = connector.config?.is_indexable as boolean;
|
const isIndexable = connector.config?.is_indexable as boolean;
|
||||||
|
|
||||||
const existingFolders =
|
const existingFolders =
|
||||||
|
|
@ -236,47 +233,48 @@ export const ComposioDriveConfig: FC<ConnectorConfigProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAuthExpired && (
|
{isAuthExpired && (
|
||||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||||
Your Google Drive authentication has expired. Please re-authenticate using the button below.
|
Your Google Drive authentication has expired. Please re-authenticate using the button
|
||||||
</p>
|
below.
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{isEditMode ? (
|
{isEditMode ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
|
onClick={() => setIsFolderTreeOpen(!isFolderTreeOpen)}
|
||||||
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
|
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors w-fit"
|
||||||
>
|
>
|
||||||
Change Selection
|
Change Selection
|
||||||
{isFolderTreeOpen ? (
|
{isFolderTreeOpen ? (
|
||||||
<ChevronDown className="size-4" />
|
<ChevronDown className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="size-4" />
|
<ChevronRight className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isFolderTreeOpen && (
|
||||||
|
<ComposioDriveFolderTree
|
||||||
|
connectorId={connector.id}
|
||||||
|
selectedFolders={selectedFolders}
|
||||||
|
onSelectFolders={handleSelectFolders}
|
||||||
|
selectedFiles={selectedFiles}
|
||||||
|
onSelectFiles={handleSelectFiles}
|
||||||
|
onAuthError={handleAuthError}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
{isFolderTreeOpen && (
|
) : (
|
||||||
<ComposioDriveFolderTree
|
<ComposioDriveFolderTree
|
||||||
connectorId={connector.id}
|
connectorId={connector.id}
|
||||||
selectedFolders={selectedFolders}
|
selectedFolders={selectedFolders}
|
||||||
onSelectFolders={handleSelectFolders}
|
onSelectFolders={handleSelectFolders}
|
||||||
selectedFiles={selectedFiles}
|
selectedFiles={selectedFiles}
|
||||||
onSelectFiles={handleSelectFiles}
|
onSelectFiles={handleSelectFiles}
|
||||||
onAuthError={handleAuthError}
|
onAuthError={handleAuthError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ComposioDriveFolderTree
|
|
||||||
connectorId={connector.id}
|
|
||||||
selectedFolders={selectedFolders}
|
|
||||||
onSelectFolders={handleSelectFolders}
|
|
||||||
selectedFiles={selectedFiles}
|
|
||||||
onSelectFiles={handleSelectFiles}
|
|
||||||
onAuthError={handleAuthError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indexing Options */}
|
{/* Indexing Options */}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { KeyRound, Server } from "lucide-react";
|
import { KeyRound, Server } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useId, useRef, useState } from "react";
|
import { useEffect, useId, useRef, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import type { ConnectorConfigProps } from "../index";
|
import type { ConnectorConfigProps } from "../index";
|
||||||
|
|
||||||
export interface ElasticsearchConfigProps extends ConnectorConfigProps {
|
export interface ElasticsearchConfigProps extends ConnectorConfigProps {
|
||||||
|
|
|
||||||
|
|
@ -231,26 +231,25 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfi
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={openPicker}
|
onClick={openPicker}
|
||||||
disabled={pickerLoading || isAuthExpired}
|
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"
|
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" />}
|
{pickerLoading && <Spinner size="xs" className="mr-1.5" />}
|
||||||
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
{totalSelected > 0 ? "Change Selection" : "Select from Google Drive"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{pickerError && !isAuthExpired && (
|
{pickerError && !isAuthExpired && <p className="text-xs text-destructive">{pickerError}</p>}
|
||||||
<p className="text-xs text-destructive">{pickerError}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isAuthExpired && (
|
{isAuthExpired && (
|
||||||
<p className="text-xs text-amber-600 dark:text-amber-500">
|
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||||
Your Google Drive authentication has expired. Please re-authenticate using the button below.
|
Your Google Drive authentication has expired. Please re-authenticate using the button
|
||||||
</p>
|
below.
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indexing Options */}
|
{/* Indexing Options */}
|
||||||
|
|
|
||||||
|
|
@ -220,10 +220,8 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Quick Index Button - hidden when auth is expired */}
|
{/* Quick Index Button - hidden when auth is expired */}
|
||||||
{connector.is_indexable &&
|
{connector.is_indexable && onQuickIndex && !isAuthExpired && (
|
||||||
onQuickIndex &&
|
|
||||||
!isAuthExpired && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -401,31 +399,31 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
||||||
Disconnect
|
Disconnect
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isAuthExpired && reauthEndpoint ? (
|
{isAuthExpired && reauthEndpoint ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleReauth}
|
onClick={handleReauth}
|
||||||
disabled={reauthing || isDisconnecting}
|
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"
|
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")} />
|
<RefreshCw className={cn("size-3.5", reauthing && "animate-spin")} />
|
||||||
Re-authenticate
|
Re-authenticate
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
disabled={isSaving || isDisconnecting}
|
disabled={isSaving || isDisconnecting}
|
||||||
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
|
||||||
>
|
>
|
||||||
{isSaving ? (
|
{isSaving ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" className="mr-2" />
|
||||||
Saving
|
Saving
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Save Changes"
|
"Save Changes"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -261,35 +261,28 @@ export const useConnectorDialog = () => {
|
||||||
| (typeof COMPOSIO_CONNECTORS)[number]
|
| (typeof COMPOSIO_CONNECTORS)[number]
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (result.connectorId) {
|
if (result.connectorId) {
|
||||||
const connectorId = parseInt(result.connectorId, 10);
|
const connectorId = parseInt(result.connectorId, 10);
|
||||||
newConnector = fetchResult.data.find(
|
newConnector = fetchResult.data.find((c: SearchSourceConnector) => c.id === connectorId);
|
||||||
(c: SearchSourceConnector) => c.id === connectorId
|
if (newConnector) {
|
||||||
);
|
const connectorType = newConnector.connector_type;
|
||||||
if (newConnector) {
|
oauthConnector =
|
||||||
const connectorType = newConnector.connector_type;
|
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||||
oauthConnector =
|
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
|
||||||
OAUTH_CONNECTORS.find(
|
}
|
||||||
(c) => c.connectorType === connectorType
|
|
||||||
) ||
|
|
||||||
COMPOSIO_CONNECTORS.find(
|
|
||||||
(c) => c.connectorType === connectorType
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!newConnector && result.connector) {
|
if (!newConnector && result.connector) {
|
||||||
oauthConnector =
|
oauthConnector =
|
||||||
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
|
OAUTH_CONNECTORS.find((c) => c.id === result.connector) ||
|
||||||
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
|
COMPOSIO_CONNECTORS.find((c) => c.id === result.connector);
|
||||||
if (oauthConnector) {
|
if (oauthConnector) {
|
||||||
const oauthType = oauthConnector.connectorType;
|
const oauthType = oauthConnector.connectorType;
|
||||||
newConnector = fetchResult.data.find(
|
newConnector = fetchResult.data.find(
|
||||||
(c: SearchSourceConnector) =>
|
(c: SearchSourceConnector) => c.connector_type === oauthType
|
||||||
c.connector_type === oauthType
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (newConnector && oauthConnector) {
|
if (newConnector && oauthConnector) {
|
||||||
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
const connectorValidation = searchSourceConnector.safeParse(newConnector);
|
||||||
|
|
@ -599,17 +592,17 @@ export const useConnectorDialog = () => {
|
||||||
: `${connectorTitle} connected and syncing started!`;
|
: `${connectorTitle} connected and syncing started!`;
|
||||||
toast.success(successMessage);
|
toast.success(successMessage);
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
setIndexingConfig(null);
|
setIndexingConfig(null);
|
||||||
setIndexingConnector(null);
|
setIndexingConnector(null);
|
||||||
setIndexingConnectorConfig(null);
|
setIndexingConnectorConfig(null);
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetchAllConnectors();
|
await refetchAllConnectors();
|
||||||
} else {
|
} else {
|
||||||
// Non-indexable connector
|
// Non-indexable connector
|
||||||
// For Circleback, transition to edit view to show webhook URL
|
// For Circleback, transition to edit view to show webhook URL
|
||||||
|
|
@ -631,11 +624,11 @@ export const useConnectorDialog = () => {
|
||||||
setStartDate(undefined);
|
setStartDate(undefined);
|
||||||
setEndDate(undefined);
|
setEndDate(undefined);
|
||||||
|
|
||||||
toast.success(`${connectorTitle} connected successfully!`, {
|
toast.success(`${connectorTitle} connected successfully!`, {
|
||||||
description: "Configure the webhook URL in your Circleback settings.",
|
description: "Configure the webhook URL in your Circleback settings.",
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetchAllConnectors();
|
await refetchAllConnectors();
|
||||||
} else {
|
} else {
|
||||||
// Other non-indexable connectors - just show success message and close
|
// Other non-indexable connectors - just show success message and close
|
||||||
const successMessage =
|
const successMessage =
|
||||||
|
|
@ -644,13 +637,13 @@ export const useConnectorDialog = () => {
|
||||||
: `${connectorTitle} connected successfully!`;
|
: `${connectorTitle} connected successfully!`;
|
||||||
toast.success(successMessage);
|
toast.success(successMessage);
|
||||||
|
|
||||||
await refetchAllConnectors();
|
await refetchAllConnectors();
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
setIndexingConfig(null);
|
setIndexingConfig(null);
|
||||||
setIndexingConnector(null);
|
setIndexingConnector(null);
|
||||||
setIndexingConnectorConfig(null);
|
setIndexingConnectorConfig(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -870,12 +863,12 @@ export const useConnectorDialog = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`${indexingConfig.connectorTitle} indexing started`);
|
toast.success(`${indexingConfig.connectorTitle} indexing started`);
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setIsFromOAuth(false);
|
setIsFromOAuth(false);
|
||||||
|
|
||||||
refreshConnectors();
|
refreshConnectors();
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||||
});
|
});
|
||||||
|
|
@ -927,21 +920,21 @@ export const useConnectorDialog = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track if we came from accounts list view so handleBackFromEdit can restore it
|
// Track if we came from accounts list view so handleBackFromEdit can restore it
|
||||||
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
|
if (viewingAccountsType && viewingAccountsType.connectorType === connector.connector_type) {
|
||||||
setCameFromAccountsList(viewingAccountsType);
|
setCameFromAccountsList(viewingAccountsType);
|
||||||
} else {
|
} else {
|
||||||
setCameFromAccountsList(null);
|
setCameFromAccountsList(null);
|
||||||
}
|
}
|
||||||
setViewingAccountsType(null);
|
setViewingAccountsType(null);
|
||||||
|
|
||||||
// Track if we came from MCP list view so handleBackFromEdit can restore it
|
// Track if we came from MCP list view so handleBackFromEdit can restore it
|
||||||
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
|
if (viewingMCPList && connector.connector_type === "MCP_CONNECTOR") {
|
||||||
setCameFromMCPList(true);
|
setCameFromMCPList(true);
|
||||||
} else {
|
} else {
|
||||||
setCameFromMCPList(false);
|
setCameFromMCPList(false);
|
||||||
}
|
}
|
||||||
setViewingMCPList(false);
|
setViewingMCPList(false);
|
||||||
|
|
||||||
// Track index with date range opened event
|
// Track index with date range opened event
|
||||||
if (connector.is_indexable) {
|
if (connector.is_indexable) {
|
||||||
|
|
@ -952,15 +945,15 @@ export const useConnectorDialog = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditingConnector(connector);
|
setEditingConnector(connector);
|
||||||
setConnectorName(connector.name);
|
setConnectorName(connector.name);
|
||||||
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
|
setPeriodicEnabled(!connector.is_indexable ? false : connector.periodic_indexing_enabled);
|
||||||
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
|
setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440");
|
||||||
setEnableSummary(connector.enable_summary ?? false);
|
setEnableSummary(connector.enable_summary ?? false);
|
||||||
setStartDate(undefined);
|
setStartDate(undefined);
|
||||||
setEndDate(undefined);
|
setEndDate(undefined);
|
||||||
},
|
},
|
||||||
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle saving connector changes
|
// Handle saving connector changes
|
||||||
|
|
@ -1139,35 +1132,35 @@ export const useConnectorDialog = () => {
|
||||||
: indexingDescription,
|
: indexingDescription,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
refreshConnectors();
|
refreshConnectors();
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving connector:", error);
|
console.error("Error saving connector:", error);
|
||||||
toast.error("Failed to save connector changes");
|
toast.error("Failed to save connector changes");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
editingConnector,
|
editingConnector,
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
isSaving,
|
isSaving,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
indexConnector,
|
indexConnector,
|
||||||
updateConnector,
|
updateConnector,
|
||||||
periodicEnabled,
|
periodicEnabled,
|
||||||
frequencyMinutes,
|
frequencyMinutes,
|
||||||
enableSummary,
|
enableSummary,
|
||||||
getFrequencyLabel,
|
getFrequencyLabel,
|
||||||
connectorConfig,
|
connectorConfig,
|
||||||
connectorName,
|
connectorName,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle disconnecting connector
|
// Handle disconnecting connector
|
||||||
|
|
@ -1194,19 +1187,19 @@ export const useConnectorDialog = () => {
|
||||||
: `${editingConnector.name} disconnected successfully`
|
: `${editingConnector.name} disconnected successfully`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
|
if (editingConnector.connector_type === "MCP_CONNECTOR" && cameFromMCPList) {
|
||||||
setViewingMCPList(true);
|
setViewingMCPList(true);
|
||||||
setEditingConnector(null);
|
setEditingConnector(null);
|
||||||
setConnectorName(null);
|
setConnectorName(null);
|
||||||
setConnectorConfig(null);
|
setConnectorConfig(null);
|
||||||
} else {
|
} else {
|
||||||
setEditingConnector(null);
|
setEditingConnector(null);
|
||||||
setConnectorName(null);
|
setConnectorName(null);
|
||||||
setConnectorConfig(null);
|
setConnectorConfig(null);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshConnectors();
|
refreshConnectors();
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||||
});
|
});
|
||||||
|
|
@ -1312,13 +1305,13 @@ export const useConnectorDialog = () => {
|
||||||
setEditingConnector(null);
|
setEditingConnector(null);
|
||||||
setConnectorName(null);
|
setConnectorName(null);
|
||||||
setConnectorConfig(null);
|
setConnectorConfig(null);
|
||||||
setConnectingConnectorType(null);
|
setConnectingConnectorType(null);
|
||||||
setViewingAccountsType(null);
|
setViewingAccountsType(null);
|
||||||
setViewingMCPList(false);
|
setViewingMCPList(false);
|
||||||
setCameFromAccountsList(null);
|
setCameFromAccountsList(null);
|
||||||
setCameFromMCPList(false);
|
setCameFromMCPList(false);
|
||||||
setConnectCameFromMCPList(false);
|
setConnectCameFromMCPList(false);
|
||||||
setStartDate(undefined);
|
setStartDate(undefined);
|
||||||
setEndDate(undefined);
|
setEndDate(undefined);
|
||||||
setPeriodicEnabled(false);
|
setPeriodicEnabled(false);
|
||||||
setFrequencyMinutes("1440");
|
setFrequencyMinutes("1440");
|
||||||
|
|
|
||||||
|
|
@ -203,8 +203,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{typeConnectors.map((connector) => {
|
{typeConnectors.map((connector) => {
|
||||||
const isIndexing = indexingConnectorIds.has(connector.id);
|
const isIndexing = indexingConnectorIds.has(connector.id);
|
||||||
const isAuthExpired =
|
const isAuthExpired = !!reauthEndpoint && connector.config?.auth_expired === true;
|
||||||
!!reauthEndpoint && connector.config?.auth_expired === true;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -252,7 +251,9 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
||||||
onClick={() => handleReauth(connector.id)}
|
onClick={() => handleReauth(connector.id)}
|
||||||
disabled={reauthingId === 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
|
Re-authenticate
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
|
||||||
|
|
||||||
const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress");
|
const inProgressStep = steps.find((s) => getEffectiveStatus(s) === "in_progress");
|
||||||
const allCompleted =
|
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;
|
const isProcessing = isThreadRunning && !allCompleted;
|
||||||
|
|
||||||
// Auto-collapse when all tasks are completed
|
// Auto-collapse when all tasks are completed
|
||||||
|
|
@ -127,7 +129,7 @@ export const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?:
|
||||||
effectiveStatus === "pending" && "text-muted-foreground/60"
|
effectiveStatus === "pending" && "text-muted-foreground/60"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{step.title}
|
{step.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step items (sub-content) */}
|
{/* Step items (sub-content) */}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,11 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
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 type { Document } from "@/contracts/types/document.types";
|
||||||
import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
||||||
import { useCommentsElectric } from "@/hooks/use-comments-electric";
|
import { useCommentsElectric } from "@/hooks/use-comments-electric";
|
||||||
|
|
@ -735,71 +739,75 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
|
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
|
||||||
{groupedTools.filter((g) => !g.connectorIcon).map((group) => (
|
{groupedTools
|
||||||
<div key={group.label}>
|
.filter((g) => !g.connectorIcon)
|
||||||
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
.map((group) => (
|
||||||
{group.label}
|
<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>
|
</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) && (
|
{groupedTools.some((g) => g.connectorIcon) && (
|
||||||
<div>
|
<div>
|
||||||
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
<div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
|
||||||
Connector Actions
|
Connector Actions
|
||||||
</div>
|
</div>
|
||||||
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
|
{groupedTools
|
||||||
const iconKey = group.connectorIcon ?? "";
|
.filter((g) => g.connectorIcon)
|
||||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
.map((group) => {
|
||||||
const toolNames = group.tools.map((t) => t.name);
|
const iconKey = group.connectorIcon ?? "";
|
||||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||||
return (
|
const toolNames = group.tools.map((t) => t.name);
|
||||||
<div
|
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||||
key={group.label}
|
return (
|
||||||
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
<div
|
||||||
>
|
key={group.label}
|
||||||
{iconInfo ? (
|
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
|
||||||
<Image
|
>
|
||||||
src={iconInfo.src}
|
{iconInfo ? (
|
||||||
alt={iconInfo.alt}
|
<Image
|
||||||
width={18}
|
src={iconInfo.src}
|
||||||
height={18}
|
alt={iconInfo.alt}
|
||||||
className="size-[18px] shrink-0 select-none pointer-events-none"
|
width={18}
|
||||||
draggable={false}
|
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"
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<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 && (
|
{!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"})`,
|
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) => (
|
{groupedTools
|
||||||
<div key={group.label}>
|
.filter((g) => !g.connectorIcon)
|
||||||
<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">
|
.map((group) => (
|
||||||
{group.label}
|
<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>
|
</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) && (
|
{groupedTools.some((g) => g.connectorIcon) && (
|
||||||
<div>
|
<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">
|
<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
|
Connector Actions
|
||||||
</div>
|
</div>
|
||||||
{groupedTools.filter((g) => g.connectorIcon).map((group) => {
|
{groupedTools
|
||||||
const iconKey = group.connectorIcon ?? "";
|
.filter((g) => g.connectorIcon)
|
||||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
.map((group) => {
|
||||||
const toolNames = group.tools.map((t) => t.name);
|
const iconKey = group.connectorIcon ?? "";
|
||||||
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||||
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
|
const toolNames = group.tools.map((t) => t.name);
|
||||||
const row = (
|
const allDisabled = toolNames.every((n) => disabledTools.includes(n));
|
||||||
<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">
|
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
|
||||||
{iconInfo ? (
|
const row = (
|
||||||
<Image
|
<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">
|
||||||
src={iconInfo.src}
|
{iconInfo ? (
|
||||||
alt={iconInfo.alt}
|
<Image
|
||||||
width={16}
|
src={iconInfo.src}
|
||||||
height={16}
|
alt={iconInfo.alt}
|
||||||
className="size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
|
width={16}
|
||||||
draggable={false}
|
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"
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<Wrench className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
|
);
|
||||||
)}
|
return (
|
||||||
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
|
<Tooltip key={group.label}>
|
||||||
{group.label}
|
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
||||||
</span>
|
<TooltipContent side="right" className="max-w-72 text-xs">
|
||||||
<Switch
|
{groupDef?.tooltip ??
|
||||||
checked={!allDisabled}
|
group.tools.map((t) => t.description).join(" · ")}
|
||||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
</TooltipContent>
|
||||||
className="shrink-0 scale-[0.6] sm:scale-75"
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!filteredTools?.length && (
|
{!filteredTools?.length && (
|
||||||
|
|
|
||||||
|
|
@ -78,14 +78,21 @@ export function ComposioDriveFolderTree({
|
||||||
}: ComposioDriveFolderTreeProps) {
|
}: ComposioDriveFolderTreeProps) {
|
||||||
const [itemStates, setItemStates] = useState<Map<string, ItemTreeNode>>(new Map());
|
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,
|
connectorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rootError && onAuthError) {
|
if (rootError && onAuthError) {
|
||||||
const msg = rootError instanceof Error ? rootError.message : String(rootError);
|
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);
|
onAuthError(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -363,19 +370,21 @@ export function ComposioDriveFolderTree({
|
||||||
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
|
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isLoadingRoot && rootError && (
|
{!isLoadingRoot && rootError && (
|
||||||
<div className="text-center text-xs sm:text-sm text-amber-600 dark:text-amber-500 py-4 sm:py-8">
|
<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")
|
{(rootError instanceof Error ? rootError.message : String(rootError)).includes(
|
||||||
? "Google Drive authentication has expired. Please re-authenticate above."
|
"authentication expired"
|
||||||
: "Failed to load Google Drive contents."}
|
)
|
||||||
</div>
|
? "Google Drive authentication has expired. Please re-authenticate above."
|
||||||
)}
|
: "Failed to load Google Drive contents."}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
|
{!isLoadingRoot && !rootError && rootItems.length === 0 && (
|
||||||
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4 sm:py-8">
|
<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
|
No files or folders found in your Google Drive
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { TagInput, type Tag as TagType } from "emblor";
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { TagInput, type Tag as TagType } from "emblor";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { CalendarIcon, XIcon } from "lucide-react";
|
import { CalendarIcon, XIcon } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "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 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 { PlateEditor } from "@/components/editor/plate-editor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
@ -58,14 +55,9 @@ function EmailsTagField({
|
||||||
onChangeRef.current(tagsToEmailString(tags));
|
onChangeRef.current(tagsToEmailString(tags));
|
||||||
}, [tags]);
|
}, [tags]);
|
||||||
|
|
||||||
const handleSetTags = useCallback(
|
const handleSetTags = useCallback((newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
|
||||||
(newTags: TagType[] | ((prev: TagType[]) => TagType[])) => {
|
setTags((prev) => (typeof newTags === "function" ? newTags(prev) : newTags));
|
||||||
setTags((prev) =>
|
}, []);
|
||||||
typeof newTags === "function" ? newTags(prev) : newTags
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAddTag = useCallback(
|
const handleAddTag = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
|
|
@ -265,7 +257,10 @@ export function HitlEditPanelContent({
|
||||||
<div className="flex flex-col gap-3 px-4 py-3 border-b">
|
<div className="flex flex-col gap-3 px-4 py-3 border-b">
|
||||||
{extraFields.map((field) => (
|
{extraFields.map((field) => (
|
||||||
<div key={field.key} className="flex flex-col gap-1.5">
|
<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}
|
{field.label}
|
||||||
</Label>
|
</Label>
|
||||||
{field.type === "emails" ? (
|
{field.type === "emails" ? (
|
||||||
|
|
@ -360,9 +355,7 @@ function MobileHitlEditDrawer() {
|
||||||
overlayClassName="z-80"
|
overlayClassName="z-80"
|
||||||
>
|
>
|
||||||
<DrawerHandle />
|
<DrawerHandle />
|
||||||
<DrawerTitle className="sr-only">
|
<DrawerTitle className="sr-only">Edit {panelState.toolName}</DrawerTitle>
|
||||||
Edit {panelState.toolName}
|
|
||||||
</DrawerTitle>
|
|
||||||
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
<div className="min-h-0 flex-1 flex flex-col overflow-hidden">
|
||||||
<HitlEditPanelContent
|
<HitlEditPanelContent
|
||||||
title={panelState.title}
|
title={panelState.title}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
|
import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
@ -22,6 +21,10 @@ import {
|
||||||
userSettingsDialogAtom,
|
userSettingsDialogAtom,
|
||||||
} from "@/atoms/settings/settings-dialog.atoms";
|
} from "@/atoms/settings/settings-dialog.atoms";
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -42,7 +45,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||||
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
||||||
import { useInbox } from "@/hooks/use-inbox";
|
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 { cleanupElectric } from "@/lib/electric/client";
|
||||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
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 type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
|
||||||
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
import { CreateSearchSpaceDialog } from "../ui/dialogs";
|
||||||
import { LayoutShell } from "../ui/shell";
|
import { LayoutShell } from "../ui/shell";
|
||||||
|
|
@ -822,11 +821,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
disabled={isDeletingChat}
|
disabled={isDeletingChat}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
|
||||||
>
|
>
|
||||||
{isDeletingChat ? (
|
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
|
||||||
<Spinner size="sm" />
|
|
||||||
) : (
|
|
||||||
tCommon("delete")
|
|
||||||
)}
|
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,13 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
||||||
} else if (effectiveTab === "report" && !reportOpen) {
|
} else if (effectiveTab === "report" && !reportOpen) {
|
||||||
effectiveTab = editorOpen ? "editor" : "sources";
|
effectiveTab = editorOpen ? "editor" : "sources";
|
||||||
} else if (effectiveTab === "sources" && !documentsOpen) {
|
} 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];
|
const targetWidth = PANEL_WIDTHS[effectiveTab];
|
||||||
|
|
@ -160,14 +166,14 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
|
||||||
)}
|
)}
|
||||||
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
|
{effectiveTab === "hitl-edit" && hitlEditOpen && hitlEditState.onSave && (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<HitlEditPanelContent
|
<HitlEditPanelContent
|
||||||
title={hitlEditState.title}
|
title={hitlEditState.title}
|
||||||
content={hitlEditState.content}
|
content={hitlEditState.content}
|
||||||
toolName={hitlEditState.toolName}
|
toolName={hitlEditState.toolName}
|
||||||
extraFields={hitlEditState.extraFields}
|
extraFields={hitlEditState.extraFields}
|
||||||
onSave={hitlEditState.onSave}
|
onSave={hitlEditState.onSave}
|
||||||
onClose={closeHitlEdit}
|
onClose={closeHitlEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -36,7 +37,6 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -36,7 +37,6 @@ import {
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||||
import { useLongPress } from "@/hooks/use-long-press";
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import {
|
import {
|
||||||
createImageGenConfigMutationAtom,
|
createImageGenConfigMutationAtom,
|
||||||
deleteImageGenConfigMutationAtom,
|
deleteImageGenConfigMutationAtom,
|
||||||
|
|
@ -38,6 +37,7 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
|
|
@ -69,12 +69,12 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
|
||||||
import {
|
import {
|
||||||
getImageGenModelsByProvider,
|
getImageGenModelsByProvider,
|
||||||
IMAGE_GEN_PROVIDERS,
|
IMAGE_GEN_PROVIDERS,
|
||||||
} from "@/contracts/enums/image-gen-providers";
|
} from "@/contracts/enums/image-gen-providers";
|
||||||
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
|
import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types";
|
||||||
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { getProviderIcon } from "@/lib/provider-icons";
|
import { getProviderIcon } from "@/lib/provider-icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
Wand2,
|
Wand2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useMemo, useState } from "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 { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||||
import {
|
import {
|
||||||
createNewLLMConfigMutationAtom,
|
createNewLLMConfigMutationAtom,
|
||||||
|
|
@ -36,6 +35,7 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useTranslations } from "next-intl";
|
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 { teamDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { TeamContent } from "@/app/dashboard/[search_space_id]/team/team-content";
|
|
||||||
|
|
||||||
interface TeamDialogProps {
|
interface TeamDialogProps {
|
||||||
searchSpaceId: number;
|
searchSpaceId: number;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { KeyRound, User } from "lucide-react";
|
import { KeyRound, User } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
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 { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent";
|
||||||
import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent";
|
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";
|
import { SettingsDialog } from "@/components/settings/settings-dialog";
|
||||||
|
|
||||||
export function UserSettingsDialog() {
|
export function UserSettingsDialog() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -11,11 +15,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface ConfluenceAccount {
|
interface ConfluenceAccount {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -108,9 +108,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsufficientPermissionsResult(
|
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||||
result: unknown,
|
|
||||||
): result is InsufficientPermissionsResult {
|
|
||||||
return (
|
return (
|
||||||
typeof result === "object" &&
|
typeof result === "object" &&
|
||||||
result !== null &&
|
result !== null &&
|
||||||
|
|
@ -161,7 +159,7 @@ function ApprovalCard({
|
||||||
space_id: selectedSpaceId || null,
|
space_id: selectedSpaceId || null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits],
|
[args.title, args.content, selectedAccountId, selectedSpaceId, pendingEdits]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleApprove = useCallback(() => {
|
const handleApprove = useCallback(() => {
|
||||||
|
|
@ -177,7 +175,17 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
canApprove,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
pendingEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -202,15 +210,16 @@ function ApprovalCard({
|
||||||
: "Create Confluence Page"}
|
: "Create Confluence Page"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "Page created with your changes" : "Page created"}
|
{pendingEdits ? "Page created with your changes" : "Page created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Page creation was cancelled</p>
|
||||||
Page creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -225,8 +234,8 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
openHitlEditPanel({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.title ?? (args.title ?? ""),
|
title: pendingEdits?.title ?? args.title ?? "",
|
||||||
content: pendingEdits?.content ?? (args.content ?? ""),
|
content: pendingEdits?.content ?? args.content ?? "",
|
||||||
toolName: "Confluence Page",
|
toolName: "Confluence Page",
|
||||||
onSave: (newTitle, newContent) => {
|
onSave: (newTitle, newContent) => {
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
|
|
@ -290,10 +299,7 @@ function ApprovalCard({
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Space <span className="text-destructive">*</span>
|
Space <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select value={selectedSpaceId} onValueChange={setSelectedSpaceId}>
|
||||||
value={selectedSpaceId}
|
|
||||||
onValueChange={setSelectedSpaceId}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select a space" />
|
<SelectValue placeholder="Select a space" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -379,9 +385,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">All Confluence accounts expired</p>
|
||||||
All Confluence accounts expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -391,9 +395,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InsufficientPermissionsCard({
|
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||||
result,
|
|
||||||
}: { result: InsufficientPermissionsResult }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
|
|
@ -474,7 +476,8 @@ export const CreateConfluencePageToolUI = makeAssistantToolUI<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthErrorResult(result)) return <AuthErrorCard 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} />;
|
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||||
|
|
||||||
return <SuccessCard result={result as SuccessResult} />;
|
return <SuccessCard result={result as SuccessResult} />;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { CornerDownLeftIcon } from "lucide-react";
|
import { CornerDownLeftIcon } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface InterruptResult {
|
interface InterruptResult {
|
||||||
|
|
@ -132,9 +132,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsufficientPermissionsResult(
|
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||||
result: unknown,
|
|
||||||
): result is InsufficientPermissionsResult {
|
|
||||||
return (
|
return (
|
||||||
typeof result === "object" &&
|
typeof result === "object" &&
|
||||||
result !== null &&
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -203,9 +209,7 @@ function ApprovalCard({
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
|
<p className="text-xs text-muted-foreground mt-0.5">Page deleted</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Page deletion was cancelled</p>
|
||||||
Page deletion was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
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="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>
|
<div className="font-medium">{page.page_title}</div>
|
||||||
{page.space_id && (
|
{page.space_id && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
|
||||||
Space: {page.space_id}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -279,11 +281,7 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
|
||||||
className="rounded-lg gap-1.5"
|
|
||||||
onClick={handleApprove}
|
|
||||||
>
|
|
||||||
Approve
|
Approve
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -309,9 +307,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
|
||||||
Confluence authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -321,9 +317,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InsufficientPermissionsCard({
|
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||||
result,
|
|
||||||
}: { result: InsufficientPermissionsResult }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
|
|
@ -357,9 +351,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
|
||||||
Page not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -437,7 +429,8 @@ export const DeleteConfluencePageToolUI = makeAssistantToolUI<
|
||||||
|
|
||||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||||
if (isAuthErrorResult(result)) return <AuthErrorCard 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 (isWarningResult(result)) return <WarningCard result={result} />;
|
||||||
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "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 { PlateEditor } from "@/components/editor/plate-editor";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface InterruptResult {
|
interface InterruptResult {
|
||||||
__interrupt__: true;
|
__interrupt__: true;
|
||||||
|
|
@ -116,9 +116,7 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsufficientPermissionsResult(
|
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||||
result: unknown,
|
|
||||||
): result is InsufficientPermissionsResult {
|
|
||||||
return (
|
return (
|
||||||
typeof result === "object" &&
|
typeof result === "object" &&
|
||||||
result !== null &&
|
result !== null &&
|
||||||
|
|
@ -169,8 +167,7 @@ function ApprovalCard({
|
||||||
const canEdit = allowedDecisions.includes("edit");
|
const canEdit = allowedDecisions.includes("edit");
|
||||||
|
|
||||||
const hasProposedChanges =
|
const hasProposedChanges =
|
||||||
actionArgs.new_title || args.new_title ||
|
actionArgs.new_title || args.new_title || actionArgs.new_content || args.new_content;
|
||||||
actionArgs.new_content || args.new_content;
|
|
||||||
|
|
||||||
const buildFinalArgs = useCallback(() => {
|
const buildFinalArgs = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -196,7 +193,16 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
hasPanelEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -221,15 +227,16 @@ function ApprovalCard({
|
||||||
: "Update Confluence Page"}
|
: "Update Confluence Page"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{hasPanelEdits ? "Page updated with your changes" : "Page updated"}
|
{hasPanelEdits ? "Page updated with your changes" : "Page updated"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Page update was cancelled</p>
|
||||||
Page update was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -293,7 +300,8 @@ function ApprovalCard({
|
||||||
className="max-h-[5rem] overflow-hidden text-xs text-muted-foreground"
|
className="max-h-[5rem] overflow-hidden text-xs text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
maskImage: "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%)",
|
WebkitMaskImage:
|
||||||
|
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
|
|
@ -306,9 +314,7 @@ function ApprovalCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{page.space_id && (
|
{page.space_id && (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">Space: {page.space_id}</div>
|
||||||
Space: {page.space_id}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -322,14 +328,18 @@ function ApprovalCard({
|
||||||
{/* Content preview — proposed changes */}
|
{/* Content preview — proposed changes */}
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3">
|
<div className="px-5 pt-3">
|
||||||
{(hasProposedChanges || hasPanelEdits) ? (
|
{hasProposedChanges || hasPanelEdits ? (
|
||||||
<>
|
<>
|
||||||
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
|
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
|
||||||
<p className="text-sm font-medium text-foreground">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
{(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content)) && (
|
{(hasPanelEdits
|
||||||
|
? editedArgs.content
|
||||||
|
: (actionArgs.new_content ?? args.new_content)) && (
|
||||||
<div
|
<div
|
||||||
className="max-h-[7rem] overflow-hidden text-sm"
|
className="max-h-[7rem] overflow-hidden text-sm"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -338,7 +348,11 @@ function ApprovalCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
markdown={String(hasPanelEdits ? editedArgs.content : (actionArgs.new_content ?? args.new_content))}
|
markdown={String(
|
||||||
|
hasPanelEdits
|
||||||
|
? editedArgs.content
|
||||||
|
: (actionArgs.new_content ?? args.new_content)
|
||||||
|
)}
|
||||||
readOnly
|
readOnly
|
||||||
preset="readonly"
|
preset="readonly"
|
||||||
editorVariant="none"
|
editorVariant="none"
|
||||||
|
|
@ -393,9 +407,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Confluence authentication expired</p>
|
||||||
Confluence authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -405,9 +417,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InsufficientPermissionsCard({
|
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||||
result,
|
|
||||||
}: { result: InsufficientPermissionsResult }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
|
|
@ -441,9 +451,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Page not found</p>
|
||||||
Page not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -509,7 +517,8 @@ export const UpdateConfluencePageToolUI = makeAssistantToolUI<
|
||||||
|
|
||||||
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
if (isNotFoundResult(result)) return <NotFoundCard result={result} />;
|
||||||
if (isAuthErrorResult(result)) return <AuthErrorCard 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} />;
|
if (isErrorResult(result)) return <ErrorCard result={result} />;
|
||||||
|
|
||||||
return <SuccessCard result={result as SuccessResult} />;
|
return <SuccessCard result={result as SuccessResult} />;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { useSetAtom } from "jotai";
|
||||||
CornerDownLeftIcon,
|
import { CornerDownLeftIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||||
Pen,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -16,11 +16,6 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GmailAccount {
|
interface GmailAccount {
|
||||||
|
|
@ -132,7 +127,11 @@ function ApprovalCard({
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||||
const [pendingEdits, setPendingEdits] = useState<{
|
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);
|
} | null>(null);
|
||||||
|
|
||||||
const accounts = interruptData.context?.accounts ?? [];
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -200,16 +210,17 @@ function ApprovalCard({
|
||||||
? "Gmail Draft Approved"
|
? "Gmail Draft Approved"
|
||||||
: "Create Gmail Draft"}
|
: "Create Gmail Draft"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text={pendingEdits ? "Creating draft with your changes" : "Creating draft"} size="sm" />
|
<TextShimmerLoader
|
||||||
|
text={pendingEdits ? "Creating draft with your changes" : "Creating draft"}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "Draft created with your changes" : "Draft created"}
|
{pendingEdits ? "Draft created with your changes" : "Draft created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Draft creation was cancelled</p>
|
||||||
Draft creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -225,13 +236,28 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
const extraFields: ExtraField[] = [
|
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: "to",
|
||||||
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
|
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({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.subject ?? (args.subject ?? ""),
|
title: pendingEdits?.subject ?? args.subject ?? "",
|
||||||
content: pendingEdits?.body ?? (args.body ?? ""),
|
content: pendingEdits?.body ?? args.body ?? "",
|
||||||
toolName: "Gmail Draft",
|
toolName: "Gmail Draft",
|
||||||
extraFields,
|
extraFields,
|
||||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||||
|
|
@ -322,7 +348,9 @@ function ApprovalCard({
|
||||||
|
|
||||||
<div className="px-5 pt-1">
|
<div className="px-5 pt-1">
|
||||||
{(pendingEdits?.subject ?? args.subject) != null && (
|
{(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 && (
|
{(pendingEdits?.body ?? args.body) != null && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -398,9 +426,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||||
Gmail authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { useSetAtom } from "jotai";
|
||||||
CornerDownLeftIcon,
|
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||||
MailIcon,
|
|
||||||
Pen,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -17,11 +16,6 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GmailAccount {
|
interface GmailAccount {
|
||||||
|
|
@ -132,7 +126,11 @@ function ApprovalCard({
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||||
const [pendingEdits, setPendingEdits] = useState<{
|
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);
|
} | null>(null);
|
||||||
|
|
||||||
const accounts = interruptData.context?.accounts ?? [];
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -200,16 +209,17 @@ function ApprovalCard({
|
||||||
? "Email Sending Approved"
|
? "Email Sending Approved"
|
||||||
: "Send Email"}
|
: "Send Email"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text={pendingEdits ? "Sending email with your changes" : "Sending email"} size="sm" />
|
<TextShimmerLoader
|
||||||
|
text={pendingEdits ? "Sending email with your changes" : "Sending email"}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "Email sent with your changes" : "Email sent"}
|
{pendingEdits ? "Email sent with your changes" : "Email sent"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Email sending was cancelled</p>
|
||||||
Email sending was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -225,13 +235,28 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
const extraFields: ExtraField[] = [
|
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: "to",
|
||||||
{ key: "bcc", label: "BCC", type: "emails", value: pendingEdits?.bcc ?? args.bcc ?? "" },
|
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({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.subject ?? (args.subject ?? ""),
|
title: pendingEdits?.subject ?? args.subject ?? "",
|
||||||
content: pendingEdits?.body ?? (args.body ?? ""),
|
content: pendingEdits?.body ?? args.body ?? "",
|
||||||
toolName: "Send Email",
|
toolName: "Send Email",
|
||||||
extraFields,
|
extraFields,
|
||||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||||
|
|
@ -264,32 +289,32 @@ function ApprovalCard({
|
||||||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||||
) : (
|
) : (
|
||||||
accounts.length > 0 && (
|
accounts.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Gmail Account <span className="text-destructive">*</span>
|
Gmail Account <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
|
<Select value={selectedAccountId} onValueChange={setSelectedAccountId}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select an account" />
|
<SelectValue placeholder="Select an account" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{validAccounts.map((account) => (
|
{validAccounts.map((account) => (
|
||||||
<SelectItem key={account.id} value={String(account.id)}>
|
<SelectItem key={account.id} value={String(account.id)}>
|
||||||
{account.name}
|
{account.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{expiredAccounts.map((a) => (
|
{expiredAccounts.map((a) => (
|
||||||
<div
|
<div
|
||||||
key={a.id}
|
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"
|
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)
|
{a.name} (expired, retry after re-auth)
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -320,7 +345,9 @@ function ApprovalCard({
|
||||||
|
|
||||||
<div className="px-5 pt-1">
|
<div className="px-5 pt-1">
|
||||||
{(pendingEdits?.subject ?? args.subject) != null && (
|
{(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 && (
|
{(pendingEdits?.body ?? args.body) != null && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -396,9 +423,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||||
Gmail authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { CalendarIcon, CornerDownLeftIcon, MailIcon, UserIcon } from "lucide-react";
|
||||||
CalendarIcon,
|
|
||||||
CornerDownLeftIcon,
|
|
||||||
MailIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GmailAccount {
|
interface GmailAccount {
|
||||||
|
|
@ -192,14 +187,12 @@ function ApprovalCard({
|
||||||
? "Email Trash Approved"
|
? "Email Trash Approved"
|
||||||
: "Trash Email"}
|
: "Trash Email"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text="Trashing email" size="sm" />
|
<TextShimmerLoader text="Trashing email" size="sm" />
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
|
<p className="text-xs text-muted-foreground mt-0.5">Email trashed</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Email trash was cancelled</p>
|
||||||
Email trash was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -280,11 +273,7 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
|
||||||
className="rounded-lg gap-1.5"
|
|
||||||
onClick={handleApprove}
|
|
||||||
>
|
|
||||||
Approve
|
Approve
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -324,9 +313,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||||
Gmail authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { useSetAtom } from "jotai";
|
||||||
CornerDownLeftIcon,
|
import { CornerDownLeftIcon, MailIcon, Pen, UserIcon, UsersIcon } from "lucide-react";
|
||||||
MailIcon,
|
|
||||||
Pen,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useState } from "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 { PlateEditor } from "@/components/editor/plate-editor";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { useSetAtom } from "jotai";
|
import { Button } from "@/components/ui/button";
|
||||||
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";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GmailAccount {
|
interface GmailAccount {
|
||||||
|
|
@ -127,15 +121,12 @@ function isAuthErrorResult(result: unknown): result is AuthErrorResult {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInsufficientPermissionsResult(
|
function isInsufficientPermissionsResult(result: unknown): result is InsufficientPermissionsResult {
|
||||||
result: unknown,
|
|
||||||
): result is InsufficientPermissionsResult {
|
|
||||||
return (
|
return (
|
||||||
typeof result === "object" &&
|
typeof result === "object" &&
|
||||||
result !== null &&
|
result !== null &&
|
||||||
"status" in result &&
|
"status" in result &&
|
||||||
(result as InsufficientPermissionsResult).status ===
|
(result as InsufficientPermissionsResult).status === "insufficient_permissions"
|
||||||
"insufficient_permissions"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,17 +168,11 @@ function ApprovalCard({
|
||||||
const existingBody = context?.existing_body;
|
const existingBody = context?.existing_body;
|
||||||
|
|
||||||
const reviewConfig = interruptData.review_configs?.[0];
|
const reviewConfig = interruptData.review_configs?.[0];
|
||||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? [
|
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||||
"approve",
|
|
||||||
"reject",
|
|
||||||
];
|
|
||||||
const canEdit = allowedDecisions.includes("edit");
|
const canEdit = allowedDecisions.includes("edit");
|
||||||
|
|
||||||
const currentSubject =
|
const currentSubject =
|
||||||
pendingEdits?.subject ??
|
pendingEdits?.subject ?? args.subject ?? email?.subject ?? args.draft_subject_or_id;
|
||||||
args.subject ??
|
|
||||||
email?.subject ??
|
|
||||||
args.draft_subject_or_id;
|
|
||||||
const currentBody = pendingEdits?.body ?? args.body;
|
const currentBody = pendingEdits?.body ?? args.body;
|
||||||
const currentTo = pendingEdits?.to ?? args.to ?? "";
|
const currentTo = pendingEdits?.to ?? args.to ?? "";
|
||||||
const currentCc = pendingEdits?.cc ?? args.cc ?? "";
|
const currentCc = pendingEdits?.cc ?? args.cc ?? "";
|
||||||
|
|
@ -259,23 +244,15 @@ function ApprovalCard({
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader
|
<TextShimmerLoader
|
||||||
text={
|
text={pendingEdits ? "Updating draft with your changes" : "Updating draft"}
|
||||||
pendingEdits
|
|
||||||
? "Updating draft with your changes"
|
|
||||||
: "Updating draft"
|
|
||||||
}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits
|
{pendingEdits ? "Draft updated with your changes" : "Draft updated"}
|
||||||
? "Draft updated with your changes"
|
|
||||||
: "Draft updated"}
|
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Draft update was cancelled</p>
|
||||||
Draft update was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -310,16 +287,12 @@ function ApprovalCard({
|
||||||
value: currentBcc,
|
value: currentBcc,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
openHitlEditPanel({
|
openHitlEditPanel({
|
||||||
title: currentSubject,
|
title: currentSubject,
|
||||||
content: editableBody,
|
content: editableBody,
|
||||||
toolName: "Gmail Draft",
|
toolName: "Gmail Draft",
|
||||||
extraFields,
|
extraFields,
|
||||||
onSave: (
|
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||||
newTitle,
|
|
||||||
newContent,
|
|
||||||
extraFieldValues,
|
|
||||||
) => {
|
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
const extras = extraFieldValues ?? {};
|
const extras = extraFieldValues ?? {};
|
||||||
setPendingEdits({
|
setPendingEdits({
|
||||||
|
|
@ -346,16 +319,12 @@ function ApprovalCard({
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 space-y-4 select-none">
|
<div className="px-5 py-4 space-y-4 select-none">
|
||||||
{context.error ? (
|
{context.error ? (
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-sm text-destructive">{context.error}</p>
|
||||||
{context.error}
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{account && (
|
{account && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">Gmail Account</p>
|
||||||
Gmail Account
|
|
||||||
</p>
|
|
||||||
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||||
{account.name}
|
{account.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -364,15 +333,11 @@ function ApprovalCard({
|
||||||
|
|
||||||
{email && (
|
{email && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">Draft to Update</p>
|
||||||
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="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">
|
<div className="flex items-center gap-1.5">
|
||||||
<MailIcon className="size-3 shrink-0 text-muted-foreground" />
|
<MailIcon className="size-3 shrink-0 text-muted-foreground" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">{email.subject}</span>
|
||||||
{email.subject}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -408,18 +373,14 @@ function ApprovalCard({
|
||||||
|
|
||||||
<div className="px-5 pt-1">
|
<div className="px-5 pt-1">
|
||||||
{currentSubject != null && (
|
{currentSubject != null && (
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">{currentSubject}</p>
|
||||||
{currentSubject}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{editableBody ? (
|
{editableBody ? (
|
||||||
<div
|
<div
|
||||||
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
||||||
style={{
|
style={{
|
||||||
maskImage:
|
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||||
"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
|
<PlateEditor
|
||||||
|
|
@ -477,9 +438,7 @@ function ErrorCard({ result }: { result: ErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Failed to update Gmail draft</p>
|
||||||
Failed to update Gmail draft
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -493,9 +452,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Gmail authentication expired</p>
|
||||||
Gmail authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -505,9 +462,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InsufficientPermissionsCard({
|
function InsufficientPermissionsCard({ result }: { result: InsufficientPermissionsResult }) {
|
||||||
result,
|
|
||||||
}: { result: InsufficientPermissionsResult }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
|
|
@ -577,7 +532,7 @@ export const UpdateGmailDraftToolUI = makeAssistantToolUI<
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("hitl-decision", {
|
new CustomEvent("hitl-decision", {
|
||||||
detail: { decisions: [decision] },
|
detail: { decisions: [decision] },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
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 { 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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -19,11 +16,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { 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 {
|
interface GoogleCalendarAccount {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -160,8 +153,12 @@ function ApprovalCard({
|
||||||
const [wasEdited, setWasEdited] = useState(false);
|
const [wasEdited, setWasEdited] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||||
const [pendingEdits, setPendingEdits] = useState<{
|
const [pendingEdits, setPendingEdits] = useState<{
|
||||||
summary: string; description: string; start_datetime: string;
|
summary: string;
|
||||||
end_datetime: string; location: string; attendees: string;
|
description: string;
|
||||||
|
start_datetime: string;
|
||||||
|
end_datetime: string;
|
||||||
|
location: string;
|
||||||
|
attendees: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const accounts = interruptData.context?.accounts ?? [];
|
const accounts = interruptData.context?.accounts ?? [];
|
||||||
|
|
@ -236,7 +233,19 @@ function ApprovalCard({
|
||||||
args: finalArgs,
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -250,7 +259,10 @@ function ApprovalCard({
|
||||||
|
|
||||||
const attendeesList = (args.attendees as string[]) ?? [];
|
const attendeesList = (args.attendees as string[]) ?? [];
|
||||||
const displayAttendees = pendingEdits?.attendees
|
const displayAttendees = pendingEdits?.attendees
|
||||||
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
|
? pendingEdits.attendees
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter(Boolean)
|
||||||
: attendeesList;
|
: attendeesList;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -266,16 +278,17 @@ function ApprovalCard({
|
||||||
? "Calendar Event Approved"
|
? "Calendar Event Approved"
|
||||||
: "Create Calendar Event"}
|
: "Create Calendar Event"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text={wasEdited ? "Creating event with your changes" : "Creating event"} size="sm" />
|
<TextShimmerLoader
|
||||||
|
text={wasEdited ? "Creating event with your changes" : "Creating event"}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{wasEdited ? "Event created with your changes" : "Event created"}
|
{wasEdited ? "Event created with your changes" : "Event created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Event creation was cancelled</p>
|
||||||
Event creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -291,14 +304,34 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
const extraFields: ExtraField[] = [
|
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: "start_datetime",
|
||||||
{ key: "location", label: "Location", type: "text", value: pendingEdits?.location ?? args.location ?? "" },
|
label: "Start",
|
||||||
{ key: "attendees", label: "Attendees", type: "emails", value: pendingEdits?.attendees ?? attendeesList.join(", ") },
|
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({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.summary ?? (args.summary ?? ""),
|
title: pendingEdits?.summary ?? args.summary ?? "",
|
||||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
content: pendingEdits?.description ?? args.description ?? "",
|
||||||
toolName: "Calendar Event",
|
toolName: "Calendar Event",
|
||||||
extraFields,
|
extraFields,
|
||||||
onSave: (newTitle, newContent, extraFieldValues) => {
|
onSave: (newTitle, newContent, extraFieldValues) => {
|
||||||
|
|
@ -307,10 +340,16 @@ function ApprovalCard({
|
||||||
setPendingEdits({
|
setPendingEdits({
|
||||||
summary: newTitle,
|
summary: newTitle,
|
||||||
description: newContent,
|
description: newContent,
|
||||||
start_datetime: extras.start_datetime ?? pendingEdits?.start_datetime ?? args.start_datetime ?? "",
|
start_datetime:
|
||||||
end_datetime: extras.end_datetime ?? pendingEdits?.end_datetime ?? args.end_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 ?? "",
|
location: extras.location ?? pendingEdits?.location ?? args.location ?? "",
|
||||||
attendees: extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
|
attendees:
|
||||||
|
extras.attendees ?? pendingEdits?.attendees ?? attendeesList.join(", "),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onClose: () => setIsPanelOpen(false),
|
onClose: () => setIsPanelOpen(false),
|
||||||
|
|
@ -372,7 +411,8 @@ function ApprovalCard({
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{calendars.map((cal) => (
|
{calendars.map((cal) => (
|
||||||
<SelectItem key={cal.id} value={cal.id}>
|
<SelectItem key={cal.id} value={cal.id}>
|
||||||
{cal.summary}{cal.primary ? " (primary)" : ""}
|
{cal.summary}
|
||||||
|
{cal.primary ? " (primary)" : ""}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -399,16 +439,26 @@ function ApprovalCard({
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3 pb-3 space-y-2">
|
<div className="px-5 pt-3 pb-3 space-y-2">
|
||||||
{(pendingEdits?.summary ?? args.summary) && (
|
{(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">
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
<ClockIcon className="size-3.5 shrink-0" />
|
<ClockIcon className="size-3.5 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{(pendingEdits?.start_datetime ?? args.start_datetime) ? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime) : ""}
|
{(pendingEdits?.start_datetime ?? args.start_datetime)
|
||||||
{(pendingEdits?.start_datetime ?? args.start_datetime) && (pendingEdits?.end_datetime ?? args.end_datetime) ? " — " : ""}
|
? formatDateTime(pendingEdits?.start_datetime ?? args.start_datetime)
|
||||||
{(pendingEdits?.end_datetime ?? args.end_datetime) ? formatDateTime(pendingEdits?.end_datetime ?? args.end_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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { CalendarIcon, ClockIcon, CornerDownLeftIcon, MapPinIcon } from "lucide-react";
|
||||||
CalendarIcon,
|
|
||||||
ClockIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
CornerDownLeftIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GoogleCalendarAccount {
|
interface GoogleCalendarAccount {
|
||||||
|
|
@ -220,14 +215,12 @@ function ApprovalCard({
|
||||||
? "Calendar Event Deletion Approved"
|
? "Calendar Event Deletion Approved"
|
||||||
: "Delete Calendar Event"}
|
: "Delete Calendar Event"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text="Deleting event" size="sm" />
|
<TextShimmerLoader text="Deleting event" size="sm" />
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
|
<p className="text-xs text-muted-foreground mt-0.5">Event deleted</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Event deletion was cancelled</p>
|
||||||
Event deletion was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -247,7 +240,9 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
{account && (
|
{account && (
|
||||||
<div className="space-y-2">
|
<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">
|
<div className="w-full rounded-md border border-input bg-muted/50 px-3 py-2 text-sm">
|
||||||
{account.name}
|
{account.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -315,11 +310,7 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
|
||||||
className="rounded-lg gap-1.5"
|
|
||||||
onClick={handleApprove}
|
|
||||||
>
|
|
||||||
Approve
|
Approve
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export { CreateCalendarEventToolUI } from "./create-event";
|
export { CreateCalendarEventToolUI } from "./create-event";
|
||||||
export { UpdateCalendarEventToolUI } from "./update-event";
|
|
||||||
export { DeleteCalendarEventToolUI } from "./delete-event";
|
export { DeleteCalendarEventToolUI } from "./delete-event";
|
||||||
|
export { UpdateCalendarEventToolUI } from "./update-event";
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
ClockIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
UsersIcon,
|
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
|
ClockIcon,
|
||||||
CornerDownLeftIcon,
|
CornerDownLeftIcon,
|
||||||
|
MapPinIcon,
|
||||||
Pen,
|
Pen,
|
||||||
|
UsersIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSetAtom } from "jotai";
|
import type { ExtraField } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
||||||
import { PlateEditor } from "@/components/editor/plate-editor";
|
import { PlateEditor } from "@/components/editor/plate-editor";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
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 {
|
interface GoogleCalendarAccount {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -180,8 +180,12 @@ function ApprovalCard({
|
||||||
const [wasEdited, setWasEdited] = useState(false);
|
const [wasEdited, setWasEdited] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
||||||
const [pendingEdits, setPendingEdits] = useState<{
|
const [pendingEdits, setPendingEdits] = useState<{
|
||||||
summary: string; description: string; start_datetime: string;
|
summary: string;
|
||||||
end_datetime: string; location: string; attendees: string;
|
description: string;
|
||||||
|
start_datetime: string;
|
||||||
|
end_datetime: string;
|
||||||
|
location: string;
|
||||||
|
attendees: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const reviewConfig = interruptData.review_configs[0];
|
const reviewConfig = interruptData.review_configs[0];
|
||||||
|
|
@ -196,19 +200,21 @@ function ApprovalCard({
|
||||||
const effectiveNewSummary = actionArgs.new_summary ?? args.new_summary;
|
const effectiveNewSummary = actionArgs.new_summary ?? args.new_summary;
|
||||||
const effectiveNewStartDatetime = actionArgs.new_start_datetime ?? args.new_start_datetime;
|
const effectiveNewStartDatetime = actionArgs.new_start_datetime ?? args.new_start_datetime;
|
||||||
const effectiveNewEndDatetime = actionArgs.new_end_datetime ?? args.new_end_datetime;
|
const effectiveNewEndDatetime = actionArgs.new_end_datetime ?? args.new_end_datetime;
|
||||||
const effectiveNewLocation = actionArgs.new_location !== undefined
|
const effectiveNewLocation =
|
||||||
? actionArgs.new_location
|
actionArgs.new_location !== undefined ? actionArgs.new_location : args.new_location;
|
||||||
: args.new_location;
|
const effectiveNewAttendees =
|
||||||
const effectiveNewAttendees = proposedAttendees
|
proposedAttendees ?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
|
||||||
?? (Array.isArray(args.new_attendees) ? args.new_attendees : null);
|
const effectiveNewDescription =
|
||||||
const effectiveNewDescription = actionArgs.new_description !== undefined
|
actionArgs.new_description !== undefined ? actionArgs.new_description : args.new_description;
|
||||||
? actionArgs.new_description
|
|
||||||
: args.new_description;
|
|
||||||
|
|
||||||
const changes: Array<{ label: string; oldVal: string; newVal: string }> = [];
|
const changes: Array<{ label: string; oldVal: string; newVal: string }> = [];
|
||||||
|
|
||||||
if (effectiveNewSummary && String(effectiveNewSummary) !== (event?.summary ?? "")) {
|
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 ?? "")) {
|
if (effectiveNewStartDatetime && String(effectiveNewStartDatetime) !== (event?.start ?? "")) {
|
||||||
changes.push({
|
changes.push({
|
||||||
|
|
@ -224,8 +230,15 @@ function ApprovalCard({
|
||||||
newVal: formatDateTime(String(effectiveNewEndDatetime)),
|
newVal: formatDateTime(String(effectiveNewEndDatetime)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (effectiveNewLocation !== undefined && String(effectiveNewLocation ?? "") !== (event?.location ?? "")) {
|
if (
|
||||||
changes.push({ label: "Location", oldVal: event?.location ?? "", newVal: String(effectiveNewLocation ?? "") });
|
effectiveNewLocation !== undefined &&
|
||||||
|
String(effectiveNewLocation ?? "") !== (event?.location ?? "")
|
||||||
|
) {
|
||||||
|
changes.push({
|
||||||
|
label: "Location",
|
||||||
|
oldVal: event?.location ?? "",
|
||||||
|
newVal: String(effectiveNewLocation ?? ""),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (effectiveNewAttendees) {
|
if (effectiveNewAttendees) {
|
||||||
const oldStr = currentAttendees.join(", ");
|
const oldStr = currentAttendees.join(", ");
|
||||||
|
|
@ -242,7 +255,10 @@ function ApprovalCard({
|
||||||
const buildFinalArgs = useCallback(() => {
|
const buildFinalArgs = useCallback(() => {
|
||||||
if (pendingEdits) {
|
if (pendingEdits) {
|
||||||
const attendeesArr = pendingEdits.attendees
|
const attendeesArr = pendingEdits.attendees
|
||||||
? pendingEdits.attendees.split(",").map((e) => e.trim()).filter(Boolean)
|
? pendingEdits.attendees
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter(Boolean)
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
event_id: event?.event_id,
|
event_id: event?.event_id,
|
||||||
|
|
@ -282,7 +298,16 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, isPanelOpen, allowedDecisions, setProcessing, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
isPanelOpen,
|
||||||
|
allowedDecisions,
|
||||||
|
setProcessing,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
pendingEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -308,15 +333,16 @@ function ApprovalCard({
|
||||||
: "Update Calendar Event"}
|
: "Update Calendar Event"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{wasEdited ? "Event updated with your changes" : "Event updated"}
|
{wasEdited ? "Event updated with your changes" : "Event updated"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Event update was cancelled</p>
|
||||||
Event update was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -331,24 +357,48 @@ function ApprovalCard({
|
||||||
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
|
className="rounded-lg text-muted-foreground -mt-1 -mr-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
const proposedSummary = pendingEdits?.summary
|
const proposedSummary =
|
||||||
?? (actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
|
pendingEdits?.summary ??
|
||||||
const proposedDescription = pendingEdits?.description
|
(actionArgs.new_summary ? String(actionArgs.new_summary) : (event?.summary ?? ""));
|
||||||
?? (actionArgs.new_description ? String(actionArgs.new_description) : (event?.description ?? ""));
|
const proposedDescription =
|
||||||
const proposedStart = pendingEdits?.start_datetime
|
pendingEdits?.description ??
|
||||||
?? (actionArgs.new_start_datetime ? String(actionArgs.new_start_datetime) : (event?.start ?? ""));
|
(actionArgs.new_description
|
||||||
const proposedEnd = pendingEdits?.end_datetime
|
? String(actionArgs.new_description)
|
||||||
?? (actionArgs.new_end_datetime ? String(actionArgs.new_end_datetime) : (event?.end ?? ""));
|
: (event?.description ?? ""));
|
||||||
const proposedLocation = pendingEdits?.location
|
const proposedStart =
|
||||||
?? (actionArgs.new_location !== undefined ? String(actionArgs.new_location ?? "") : (event?.location ?? ""));
|
pendingEdits?.start_datetime ??
|
||||||
const proposedAttendeesStr = pendingEdits?.attendees
|
(actionArgs.new_start_datetime
|
||||||
?? (proposedAttendees ? proposedAttendees.join(", ") : currentAttendees.join(", "));
|
? 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[] = [
|
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: "end_datetime", label: "End", type: "datetime-local", value: proposedEnd },
|
||||||
{ key: "location", label: "Location", type: "text", value: proposedLocation },
|
{ 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({
|
openHitlEditPanel({
|
||||||
title: proposedSummary,
|
title: proposedSummary,
|
||||||
|
|
@ -377,7 +427,7 @@ function ApprovalCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content section */}
|
{/* Content section */}
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 space-y-4 select-none">
|
<div className="px-5 py-4 space-y-4 select-none">
|
||||||
{context?.error ? (
|
{context?.error ? (
|
||||||
|
|
@ -433,9 +483,13 @@ function ApprovalCard({
|
||||||
<div key={change.label} className="text-xs space-y-0.5">
|
<div key={change.label} className="text-xs space-y-0.5">
|
||||||
<span className="text-muted-foreground">{change.label}</span>
|
<span className="text-muted-foreground">{change.label}</span>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<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" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -446,7 +500,8 @@ function ApprovalCard({
|
||||||
className="mt-1 max-h-[5rem] overflow-hidden"
|
className="mt-1 max-h-[5rem] overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
maskImage: "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%)",
|
WebkitMaskImage:
|
||||||
|
"linear-gradient(to bottom, black 50%, transparent 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { useSetAtom } from "jotai";
|
||||||
CornerDownLeftIcon,
|
import { CornerDownLeftIcon, FileIcon, Pen } from "lucide-react";
|
||||||
FileIcon,
|
|
||||||
Pen,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -15,11 +15,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface GoogleDriveAccount {
|
interface GoogleDriveAccount {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -139,8 +135,8 @@ function ApprovalCard({
|
||||||
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
|
const [pendingEdits, setPendingEdits] = useState<{ name: string; content: string } | null>(null);
|
||||||
|
|
||||||
const accounts = interruptData.context?.accounts ?? [];
|
const accounts = interruptData.context?.accounts ?? [];
|
||||||
const validAccounts = accounts.filter(a => !a.auth_expired);
|
const validAccounts = accounts.filter((a) => !a.auth_expired);
|
||||||
const expiredAccounts = accounts.filter(a => a.auth_expired);
|
const expiredAccounts = accounts.filter((a) => a.auth_expired);
|
||||||
|
|
||||||
const defaultAccountId = useMemo(() => {
|
const defaultAccountId = useMemo(() => {
|
||||||
if (validAccounts.length === 1) return String(validAccounts[0].id);
|
if (validAccounts.length === 1) return String(validAccounts[0].id);
|
||||||
|
|
@ -162,7 +158,8 @@ function ApprovalCard({
|
||||||
setParentFolderId("__root__");
|
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 isNameValid = useMemo(() => {
|
||||||
const name = pendingEdits?.name ?? args.name;
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -218,16 +228,17 @@ function ApprovalCard({
|
||||||
? `${fileTypeLabel} Approved`
|
? `${fileTypeLabel} Approved`
|
||||||
: `Create ${fileTypeLabel}`}
|
: `Create ${fileTypeLabel}`}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text={pendingEdits ? "Creating file with your changes" : "Creating file"} size="sm" />
|
<TextShimmerLoader
|
||||||
|
text={pendingEdits ? "Creating file with your changes" : "Creating file"}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "File created with your changes" : "File created"}
|
{pendingEdits ? "File created with your changes" : "File created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">File creation was cancelled</p>
|
||||||
File creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -242,8 +253,8 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
openHitlEditPanel({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.name ?? (args.name ?? ""),
|
title: pendingEdits?.name ?? args.name ?? "",
|
||||||
content: pendingEdits?.content ?? (args.content ?? ""),
|
content: pendingEdits?.content ?? args.content ?? "",
|
||||||
toolName: fileTypeLabel,
|
toolName: fileTypeLabel,
|
||||||
onSave: (newName, newContent) => {
|
onSave: (newName, newContent) => {
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
|
|
@ -268,33 +279,33 @@ function ApprovalCard({
|
||||||
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{accounts.length > 0 && (
|
{accounts.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Google Drive Account <span className="text-destructive">*</span>
|
Google Drive Account <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
|
<Select value={selectedAccountId} onValueChange={handleAccountChange}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select an account" />
|
<SelectValue placeholder="Select an account" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{validAccounts.map((account) => (
|
{validAccounts.map((account) => (
|
||||||
<SelectItem key={account.id} value={String(account.id)}>
|
<SelectItem key={account.id} value={String(account.id)}>
|
||||||
{account.name}
|
{account.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{expiredAccounts.map((a) => (
|
{expiredAccounts.map((a) => (
|
||||||
<div
|
<div
|
||||||
key={a.id}
|
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"
|
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)
|
{a.name} (expired, retry after re-auth)
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
|
@ -311,31 +322,29 @@ function ApprovalCard({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedAccountId && (
|
{selectedAccountId && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">Parent Folder</p>
|
||||||
Parent Folder
|
<Select value={parentFolderId} onValueChange={setParentFolderId}>
|
||||||
</p>
|
<SelectTrigger className="w-full">
|
||||||
<Select value={parentFolderId} onValueChange={setParentFolderId}>
|
<SelectValue placeholder="Drive Root" />
|
||||||
<SelectTrigger className="w-full">
|
</SelectTrigger>
|
||||||
<SelectValue placeholder="Drive Root" />
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="__root__">Drive Root</SelectItem>
|
||||||
<SelectContent>
|
{availableParentFolders.map((folder) => (
|
||||||
<SelectItem value="__root__">Drive Root</SelectItem>
|
<SelectItem key={folder.folder_id} value={folder.folder_id}>
|
||||||
{availableParentFolders.map((folder) => (
|
{folder.name}
|
||||||
<SelectItem key={folder.folder_id} value={folder.folder_id}>
|
</SelectItem>
|
||||||
{folder.name}
|
))}
|
||||||
</SelectItem>
|
</SelectContent>
|
||||||
))}
|
</Select>
|
||||||
</SelectContent>
|
{availableParentFolders.length === 0 && (
|
||||||
</Select>
|
<p className="text-xs text-muted-foreground">
|
||||||
{availableParentFolders.length === 0 && (
|
No folders found. File will be created at Drive root.
|
||||||
<p className="text-xs text-muted-foreground">
|
</p>
|
||||||
No folders found. File will be created at Drive root.
|
)}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -345,9 +354,11 @@ function ApprovalCard({
|
||||||
{/* Content preview */}
|
{/* Content preview */}
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3">
|
<div className="px-5 pt-3">
|
||||||
{(pendingEdits?.name ?? args.name) != null && (
|
{(pendingEdits?.name ?? args.name) != null && (
|
||||||
<p className="text-sm font-medium text-foreground">{String(pendingEdits?.name ?? args.name)}</p>
|
<p className="text-sm font-medium text-foreground">
|
||||||
)}
|
{String(pendingEdits?.name ?? args.name)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{(pendingEdits?.content ?? args.content) != null && (
|
{(pendingEdits?.content ?? args.content) != null && (
|
||||||
<div
|
<div
|
||||||
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
className="mt-2 max-h-[7rem] overflow-hidden text-sm"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import {
|
import { CornerDownLeftIcon, InfoIcon } from "lucide-react";
|
||||||
CornerDownLeftIcon,
|
|
||||||
InfoIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface GoogleDriveAccount {
|
interface GoogleDriveAccount {
|
||||||
|
|
@ -207,14 +204,12 @@ function ApprovalCard({
|
||||||
? "Google Drive File Deletion Approved"
|
? "Google Drive File Deletion Approved"
|
||||||
: "Delete Google Drive File"}
|
: "Delete Google Drive File"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text="Trashing file" size="sm" />
|
<TextShimmerLoader text="Trashing file" size="sm" />
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
|
<p className="text-xs text-muted-foreground mt-0.5">File trashed</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">File deletion was cancelled</p>
|
||||||
File deletion was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -274,22 +269,23 @@ function ApprovalCard({
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 space-y-3 select-none">
|
<div className="px-5 py-4 space-y-3 select-none">
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="delete-from-kb"
|
id="delete-from-kb"
|
||||||
checked={deleteFromKb}
|
checked={deleteFromKb}
|
||||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
|
<label htmlFor="delete-from-kb" className="flex-1 cursor-pointer">
|
||||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
This will permanently delete the file from your knowledge base (cannot be undone)
|
This will permanently delete the file from your knowledge base (cannot be undone)
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -298,27 +294,23 @@ function ApprovalCard({
|
||||||
{phase === "pending" && (
|
{phase === "pending" && (
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
Approve
|
||||||
className="rounded-lg gap-1.5"
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
onClick={handleApprove}
|
</Button>
|
||||||
>
|
<Button
|
||||||
Approve
|
size="sm"
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
variant="ghost"
|
||||||
</Button>
|
className="rounded-lg text-muted-foreground"
|
||||||
<Button
|
onClick={() => {
|
||||||
size="sm"
|
setRejected();
|
||||||
variant="ghost"
|
onDecision({ type: "reject", message: "User rejected the action." });
|
||||||
className="rounded-lg text-muted-foreground"
|
}}
|
||||||
onClick={() => {
|
>
|
||||||
setRejected();
|
Reject
|
||||||
onDecision({ type: "reject", message: "User rejected the action." });
|
</Button>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -11,11 +15,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface JiraAccount {
|
interface JiraAccount {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -151,7 +151,9 @@ function ApprovalCard({
|
||||||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
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 [selectedAccountId, setSelectedAccountId] = useState("");
|
||||||
const [selectedProjectKey, setSelectedProjectKey] = useState(args.project_key ?? "");
|
const [selectedProjectKey, setSelectedProjectKey] = useState(args.project_key ?? "");
|
||||||
|
|
@ -177,14 +179,23 @@ function ApprovalCard({
|
||||||
(overrides?: { title?: string; description?: string }) => {
|
(overrides?: { title?: string; description?: string }) => {
|
||||||
return {
|
return {
|
||||||
summary: overrides?.title ?? pendingEdits?.title ?? args.summary,
|
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,
|
connector_id: selectedAccountId ? Number(selectedAccountId) : null,
|
||||||
project_key: selectedProjectKey || null,
|
project_key: selectedProjectKey || null,
|
||||||
issue_type: selectedIssueType === "__none__" ? null : selectedIssueType,
|
issue_type: selectedIssueType === "__none__" ? null : selectedIssueType,
|
||||||
priority: selectedPriority === "__none__" ? null : selectedPriority,
|
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(() => {
|
const handleApprove = useCallback(() => {
|
||||||
|
|
@ -200,7 +211,17 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
canApprove,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
pendingEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -225,15 +246,16 @@ function ApprovalCard({
|
||||||
: "Create Jira Issue"}
|
: "Create Jira Issue"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
|
||||||
Issue creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -248,8 +270,8 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
openHitlEditPanel({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.title ?? (args.summary ?? ""),
|
title: pendingEdits?.title ?? args.summary ?? "",
|
||||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
content: pendingEdits?.description ?? args.description ?? "",
|
||||||
toolName: "Jira Issue",
|
toolName: "Jira Issue",
|
||||||
onSave: (newTitle, newDescription) => {
|
onSave: (newTitle, newDescription) => {
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
|
|
@ -316,10 +338,7 @@ function ApprovalCard({
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Project <span className="text-destructive">*</span>
|
Project <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select value={selectedProjectKey} onValueChange={setSelectedProjectKey}>
|
||||||
value={selectedProjectKey}
|
|
||||||
onValueChange={setSelectedProjectKey}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select a project" />
|
<SelectValue placeholder="Select a project" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -336,32 +355,26 @@ function ApprovalCard({
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Issue Type</p>
|
<p className="text-xs font-medium text-muted-foreground">Issue Type</p>
|
||||||
<Select
|
<Select value={selectedIssueType} onValueChange={setSelectedIssueType}>
|
||||||
value={selectedIssueType}
|
|
||||||
onValueChange={setSelectedIssueType}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Task" />
|
<SelectValue placeholder="Task" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{issueTypes.length > 0
|
{issueTypes.length > 0 ? (
|
||||||
? issueTypes.map((t) => (
|
issueTypes.map((t) => (
|
||||||
<SelectItem key={t.id} value={t.name}>
|
<SelectItem key={t.id} value={t.name}>
|
||||||
{t.name}
|
{t.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
: (
|
) : (
|
||||||
<SelectItem value="Task">Task</SelectItem>
|
<SelectItem value="Task">Task</SelectItem>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Priority</p>
|
<p className="text-xs font-medium text-muted-foreground">Priority</p>
|
||||||
<Select
|
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
|
||||||
value={selectedPriority}
|
|
||||||
onValueChange={setSelectedPriority}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Default" />
|
<SelectValue placeholder="Default" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -388,7 +401,9 @@ function ApprovalCard({
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3">
|
<div className="px-5 pt-3">
|
||||||
{(pendingEdits?.title ?? args.summary) != null && (
|
{(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 && (
|
{(pendingEdits?.description ?? args.description) != null && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -450,9 +465,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">All Jira accounts expired</p>
|
||||||
All Jira accounts expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { CornerDownLeftIcon } from "lucide-react";
|
import { CornerDownLeftIcon } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface JiraAccount {
|
interface JiraAccount {
|
||||||
|
|
@ -204,9 +204,7 @@ function ApprovalCard({
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
|
||||||
Issue deletion was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -280,11 +278,7 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
|
||||||
className="rounded-lg gap-1.5"
|
|
||||||
onClick={handleApprove}
|
|
||||||
>
|
|
||||||
Approve
|
Approve
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -310,9 +304,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
|
||||||
Jira authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -356,9 +348,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||||
Issue not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -13,10 +16,7 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface JiraIssue {
|
interface JiraIssue {
|
||||||
issue_id: string;
|
issue_id: string;
|
||||||
|
|
@ -194,9 +194,12 @@ function ApprovalCard({
|
||||||
const canEdit = allowedDecisions.includes("edit");
|
const canEdit = allowedDecisions.includes("edit");
|
||||||
|
|
||||||
const hasProposedChanges =
|
const hasProposedChanges =
|
||||||
actionArgs.new_summary || args.new_summary ||
|
actionArgs.new_summary ||
|
||||||
actionArgs.new_description || args.new_description ||
|
args.new_summary ||
|
||||||
actionArgs.new_priority || args.new_priority;
|
actionArgs.new_description ||
|
||||||
|
args.new_description ||
|
||||||
|
actionArgs.new_priority ||
|
||||||
|
args.new_priority;
|
||||||
|
|
||||||
const buildFinalArgs = useCallback(() => {
|
const buildFinalArgs = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -222,7 +225,16 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
hasPanelEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -247,15 +259,16 @@ function ApprovalCard({
|
||||||
: "Update Jira Issue"}
|
: "Update Jira Issue"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
|
||||||
Issue update was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -366,14 +379,20 @@ function ApprovalCard({
|
||||||
{/* Content preview — proposed changes */}
|
{/* Content preview — proposed changes */}
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3">
|
<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">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
{(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description)) && (
|
{(hasPanelEdits
|
||||||
|
? editedArgs.description
|
||||||
|
: (actionArgs.new_description ?? args.new_description)) && (
|
||||||
<div
|
<div
|
||||||
className="max-h-[7rem] overflow-hidden text-sm"
|
className="max-h-[7rem] overflow-hidden text-sm"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -382,7 +401,11 @@ function ApprovalCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
|
markdown={String(
|
||||||
|
hasPanelEdits
|
||||||
|
? editedArgs.description
|
||||||
|
: (actionArgs.new_description ?? args.new_description)
|
||||||
|
)}
|
||||||
readOnly
|
readOnly
|
||||||
preset="readonly"
|
preset="readonly"
|
||||||
editorVariant="none"
|
editorVariant="none"
|
||||||
|
|
@ -445,9 +468,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Jira authentication expired</p>
|
||||||
Jira authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -491,9 +512,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||||
Issue not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -13,11 +17,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { useSetAtom } from "jotai";
|
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface LinearLabel {
|
interface LinearLabel {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -148,7 +148,9 @@ function ApprovalCard({
|
||||||
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
const { phase, setProcessing, setRejected } = useHitlPhase(interruptData);
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
|
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 [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState("");
|
const [selectedTeamId, setSelectedTeamId] = useState("");
|
||||||
|
|
@ -178,18 +180,32 @@ function ApprovalCard({
|
||||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||||
const canEdit = allowedDecisions.includes("edit");
|
const canEdit = allowedDecisions.includes("edit");
|
||||||
|
|
||||||
const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
|
const buildFinalArgs = useCallback(
|
||||||
return {
|
(overrides?: { title?: string; description?: string }) => {
|
||||||
title: overrides?.title ?? pendingEdits?.title ?? args.title,
|
return {
|
||||||
description: overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
title: overrides?.title ?? pendingEdits?.title ?? args.title,
|
||||||
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
|
description:
|
||||||
team_id: selectedTeamId || null,
|
overrides?.description ?? pendingEdits?.description ?? args.description ?? null,
|
||||||
state_id: selectedStateId === "__none__" ? null : selectedStateId,
|
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
|
||||||
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
|
team_id: selectedTeamId || null,
|
||||||
priority: Number(selectedPriority),
|
state_id: selectedStateId === "__none__" ? null : selectedStateId,
|
||||||
label_ids: selectedLabelIds,
|
assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
|
||||||
};
|
priority: Number(selectedPriority),
|
||||||
}, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds, pendingEdits]);
|
label_ids: selectedLabelIds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[
|
||||||
|
args.title,
|
||||||
|
args.description,
|
||||||
|
selectedWorkspaceId,
|
||||||
|
selectedTeamId,
|
||||||
|
selectedStateId,
|
||||||
|
selectedAssigneeId,
|
||||||
|
selectedPriority,
|
||||||
|
selectedLabelIds,
|
||||||
|
pendingEdits,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const handleApprove = useCallback(() => {
|
const handleApprove = useCallback(() => {
|
||||||
if (phase !== "pending") return;
|
if (phase !== "pending") return;
|
||||||
|
|
@ -204,7 +220,17 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs, pendingEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
canApprove,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
pendingEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -229,15 +255,16 @@ function ApprovalCard({
|
||||||
: "Create Linear Issue"}
|
: "Create Linear Issue"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
{pendingEdits ? "Issue created with your changes" : "Issue created"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue creation was cancelled</p>
|
||||||
Issue creation was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -252,8 +279,8 @@ function ApprovalCard({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
openHitlEditPanel({
|
openHitlEditPanel({
|
||||||
title: pendingEdits?.title ?? (args.title ?? ""),
|
title: pendingEdits?.title ?? args.title ?? "",
|
||||||
content: pendingEdits?.description ?? (args.description ?? ""),
|
content: pendingEdits?.description ?? args.description ?? "",
|
||||||
toolName: "Linear Issue",
|
toolName: "Linear Issue",
|
||||||
onSave: (newTitle, newDescription) => {
|
onSave: (newTitle, newDescription) => {
|
||||||
setIsPanelOpen(false);
|
setIsPanelOpen(false);
|
||||||
|
|
@ -269,7 +296,7 @@ function ApprovalCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Context section — real pickers in pending */}
|
{/* Context section — real pickers in pending */}
|
||||||
{phase === "pending" && (
|
{phase === "pending" && (
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<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>
|
<p className="text-sm text-destructive">{interruptData.context.error}</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{workspaces.length > 0 && (
|
{workspaces.length > 0 && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs font-medium text-muted-foreground">
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
Linear Account <span className="text-destructive">*</span>
|
Linear Account <span className="text-destructive">*</span>
|
||||||
</p>
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={selectedWorkspaceId}
|
value={selectedWorkspaceId}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
setSelectedWorkspaceId(v);
|
setSelectedWorkspaceId(v);
|
||||||
setSelectedTeamId("");
|
setSelectedTeamId("");
|
||||||
setSelectedStateId("__none__");
|
setSelectedStateId("__none__");
|
||||||
setSelectedAssigneeId("__none__");
|
setSelectedAssigneeId("__none__");
|
||||||
setSelectedPriority("0");
|
setSelectedPriority("0");
|
||||||
setSelectedLabelIds([]);
|
setSelectedLabelIds([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select an account" />
|
<SelectValue placeholder="Select an account" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{validWorkspaces.map((w) => (
|
{validWorkspaces.map((w) => (
|
||||||
<SelectItem key={w.id} value={String(w.id)}>
|
<SelectItem key={w.id} value={String(w.id)}>
|
||||||
{w.name}
|
{w.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{expiredWorkspaces.map((w) => (
|
{expiredWorkspaces.map((w) => (
|
||||||
<div
|
<div
|
||||||
key={w.id}
|
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"
|
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)
|
{w.name} (expired, retry after re-auth)
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedWorkspace && (
|
{selectedWorkspace && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -366,7 +393,10 @@ function ApprovalCard({
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Assignee</p>
|
<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">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Unassigned" />
|
<SelectValue placeholder="Unassigned" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -520,9 +550,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">All Linear accounts expired</p>
|
||||||
All Linear accounts expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { CornerDownLeftIcon } from "lucide-react";
|
import { CornerDownLeftIcon } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
|
||||||
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
import { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
|
|
||||||
interface InterruptResult {
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -174,14 +182,12 @@ function ApprovalCard({
|
||||||
? "Linear Issue Deletion Approved"
|
? "Linear Issue Deletion Approved"
|
||||||
: "Delete Linear Issue"}
|
: "Delete Linear Issue"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{phase === "processing" ? (
|
||||||
<TextShimmerLoader text="Deleting issue" size="sm" />
|
<TextShimmerLoader text="Deleting issue" size="sm" />
|
||||||
) : phase === "complete" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
<p className="text-xs text-muted-foreground mt-0.5">Issue deleted</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue deletion was cancelled</p>
|
||||||
Issue deletion was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -232,20 +238,20 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 select-none">
|
<div className="px-5 py-4 select-none">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="linear-delete-from-kb"
|
id="linear-delete-from-kb"
|
||||||
checked={deleteFromKb}
|
checked={deleteFromKb}
|
||||||
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
onCheckedChange={(v) => setDeleteFromKb(v === true)}
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
|
<label htmlFor="linear-delete-from-kb" className="flex-1 cursor-pointer">
|
||||||
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
<span className="text-sm text-foreground">Also remove from knowledge base</span>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
This will permanently delete the issue from your knowledge base (cannot be undone)
|
This will permanently delete the issue from your knowledge base (cannot be undone)
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -254,12 +260,8 @@ function ApprovalCard({
|
||||||
{phase === "pending" && (
|
{phase === "pending" && (
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
<div className="px-5 py-4 flex items-center gap-2 select-none">
|
||||||
<Button
|
<Button size="sm" className="rounded-lg gap-1.5" onClick={handleApprove}>
|
||||||
size="sm"
|
|
||||||
className="rounded-lg gap-1.5"
|
|
||||||
onClick={handleApprove}
|
|
||||||
>
|
|
||||||
Approve
|
Approve
|
||||||
<CornerDownLeftIcon className="size-3 opacity-60" />
|
<CornerDownLeftIcon className="size-3 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -285,9 +287,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
|
||||||
Linear authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -315,9 +315,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||||
Issue not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
import { CornerDownLeftIcon, Pen } from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,10 +17,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
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 { useHitlPhase } from "@/hooks/use-hitl-phase";
|
||||||
import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
|
|
||||||
|
|
||||||
interface LinearLabel {
|
interface LinearLabel {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -110,7 +110,12 @@ interface AuthErrorResult {
|
||||||
connector_type: string;
|
connector_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateLinearIssueResult = InterruptResult | SuccessResult | ErrorResult | NotFoundResult | AuthErrorResult;
|
type UpdateLinearIssueResult =
|
||||||
|
| InterruptResult
|
||||||
|
| SuccessResult
|
||||||
|
| ErrorResult
|
||||||
|
| NotFoundResult
|
||||||
|
| AuthErrorResult;
|
||||||
|
|
||||||
function isInterruptResult(result: unknown): result is InterruptResult {
|
function isInterruptResult(result: unknown): result is InterruptResult {
|
||||||
return (
|
return (
|
||||||
|
|
@ -178,7 +183,9 @@ function ApprovalCard({
|
||||||
const issue = context?.issue;
|
const issue = context?.issue;
|
||||||
|
|
||||||
const initialEditState = {
|
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
|
description: actionArgs.new_description
|
||||||
? String(actionArgs.new_description)
|
? String(actionArgs.new_description)
|
||||||
: (issue?.description ?? args.new_description ?? ""),
|
: (issue?.description ?? args.new_description ?? ""),
|
||||||
|
|
@ -256,8 +263,10 @@ function ApprovalCard({
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasProposedChanges =
|
const hasProposedChanges =
|
||||||
actionArgs.new_title || args.new_title ||
|
actionArgs.new_title ||
|
||||||
actionArgs.new_description || args.new_description ||
|
args.new_title ||
|
||||||
|
actionArgs.new_description ||
|
||||||
|
args.new_description ||
|
||||||
proposedStateName ||
|
proposedStateName ||
|
||||||
proposedAssigneeName ||
|
proposedAssigneeName ||
|
||||||
proposedPriorityLabel ||
|
proposedPriorityLabel ||
|
||||||
|
|
@ -276,7 +285,16 @@ function ApprovalCard({
|
||||||
args: buildFinalArgs(),
|
args: buildFinalArgs(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [phase, setProcessing, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs, hasPanelEdits]);
|
}, [
|
||||||
|
phase,
|
||||||
|
setProcessing,
|
||||||
|
isPanelOpen,
|
||||||
|
allowedDecisions,
|
||||||
|
onDecision,
|
||||||
|
interruptData,
|
||||||
|
buildFinalArgs,
|
||||||
|
hasPanelEdits,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
|
@ -301,15 +319,16 @@ function ApprovalCard({
|
||||||
: "Update Linear Issue"}
|
: "Update Linear Issue"}
|
||||||
</p>
|
</p>
|
||||||
{phase === "processing" ? (
|
{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" ? (
|
) : phase === "complete" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
{hasPanelEdits ? "Issue updated with your changes" : "Issue updated"}
|
||||||
</p>
|
</p>
|
||||||
) : phase === "rejected" ? (
|
) : phase === "rejected" ? (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">Issue update was cancelled</p>
|
||||||
Issue update was cancelled
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Requires your approval to proceed
|
Requires your approval to proceed
|
||||||
|
|
@ -346,7 +365,7 @@ function ApprovalCard({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Context section — workspace + current issue + pickers in pending */}
|
{/* Context section — workspace + current issue + pickers in pending */}
|
||||||
{phase === "pending" && (
|
{phase === "pending" && (
|
||||||
<>
|
<>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
|
|
@ -385,7 +404,9 @@ function ApprovalCard({
|
||||||
)}
|
)}
|
||||||
{issue.current_assignee && <span>{issue.current_assignee.name}</span>}
|
{issue.current_assignee && <span>{issue.current_assignee.name}</span>}
|
||||||
{priorities.find((p) => p.priority === issue.priority) && (
|
{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>
|
</div>
|
||||||
{issue.current_labels && issue.current_labels.length > 0 && (
|
{issue.current_labels && issue.current_labels.length > 0 && (
|
||||||
|
|
@ -510,9 +531,7 @@ function ApprovalCard({
|
||||||
? `${label.color}70`
|
? `${label.color}70`
|
||||||
: `${label.color}28`,
|
: `${label.color}28`,
|
||||||
color: label.color,
|
color: label.color,
|
||||||
borderColor: isSelected
|
borderColor: isSelected ? `${label.color}cc` : "transparent",
|
||||||
? `${label.color}cc`
|
|
||||||
: "transparent",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -538,12 +557,18 @@ function ApprovalCard({
|
||||||
{/* Content preview — proposed changes */}
|
{/* Content preview — proposed changes */}
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 pt-3">
|
<div className="px-5 pt-3">
|
||||||
{(hasProposedChanges || hasPanelEdits) ? (
|
{hasProposedChanges || hasPanelEdits ? (
|
||||||
<>
|
<>
|
||||||
{(hasPanelEdits ? editedArgs.title : (actionArgs.new_title ?? args.new_title)) && (
|
{(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
|
<div
|
||||||
className="max-h-[7rem] overflow-hidden text-sm"
|
className="max-h-[7rem] overflow-hidden text-sm"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -552,7 +577,11 @@ function ApprovalCard({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
markdown={String(hasPanelEdits ? editedArgs.description : (actionArgs.new_description ?? args.new_description))}
|
markdown={String(
|
||||||
|
hasPanelEdits
|
||||||
|
? editedArgs.description
|
||||||
|
: (actionArgs.new_description ?? args.new_description)
|
||||||
|
)}
|
||||||
readOnly
|
readOnly
|
||||||
preset="readonly"
|
preset="readonly"
|
||||||
editorVariant="none"
|
editorVariant="none"
|
||||||
|
|
@ -641,9 +670,7 @@ function AuthErrorCard({ result }: { result: AuthErrorResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-destructive">
|
<p className="text-sm font-semibold text-destructive">Linear authentication expired</p>
|
||||||
Linear authentication expired
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
|
|
@ -671,9 +698,7 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
<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">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<p className="text-sm font-semibold text-amber-600 dark:text-amber-400">Issue not found</p>
|
||||||
Issue not found
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 h-px bg-border/50" />
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="px-5 py-4">
|
<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