feat: add sessions endpoint with session management enhancements (#8596)
* 📝 (monitor.py): Add endpoint to get sessions and handle session_id encoding for API requests 📝 (use-get-messages-mutation.ts): Implement a mutation function to fetch messages with query parameters and handle session_id encoding for API requests 📝 (use-get-messages-polling.ts): Ensure proper encoding of session_id for API requests in polling mutation 📝 (use-get-messages.ts): Handle session_id encoding for API requests in messages query 📝 (new-modal.tsx): Implement functions to handle session deletion and proper encoding of session_id for API requests 📝 (utils.ts): Add functions to encode, decode, validate, format, and prepare session IDs for API requests * 📝 (constants.ts): Add SESSIONS constant to API URLs for monitoring sessions 🔧 (use-delete-messages.ts): Add queryClient to UseRequestProcessor to invalidate sessions query ✨ (use-get-sessions-from-flow.ts): Introduce useGetSessionsFromFlowQuery to fetch sessions from flow 🔧 (use-rename-session.ts): Change refetchQueries to invalidateQueries for useGetSessionsFromFlowQuery 🔧 (custom-new-modal.tsx): Update import path for IOModal to playground-modal 🔧 (session-selector.tsx): Add setActiveSession function to handle setting active session 🔧 (sidebar-open-view.tsx): Add setActiveSession function to handle setting active session ♻️ (new-modal.tsx): Refactor IOModal into playground-modal and update functionality ♻️ (playground-modal.tsx): Refactor IOModal to handle playground-specific functionality ⬆️ (flowStore.ts): Add newChatOnPlayground state and setNewChatOnPlayground function ⬆️ (index.ts): Update FlowStoreType to include newChatOnPlayground and setNewChatOnPlayground * 🔧 (pyproject.toml): update testpaths to point to the correct directory for tests ✨ (test_session_endpoint.py): add unit tests for sessions endpoint with flow_id filtering ♻️ (session-selector.tsx): refactor to trim editedSession before setting it ♻️ (sidebar-open-view.tsx): refactor to set visibleSession instead of activeSession * ✨ (use-get-sessions-from-flow.ts): Always include the flow ID as the default session if it's not already present ♻️ (playground-modal.tsx): Refactor setting sessions to include currentFlowId as the default session if not present, and handle visibility of sessions more efficiently * ♻️ (use-get-messages-mutation.ts): remove unused imports and refactor code for better readability and maintainability * ✨ (test_session_endpoint.py): refactor test function names for better clarity and consistency * ✨ (create-new-session-name.ts): add function to generate a new session name based on the current date and time 🔧 (playground-modal.tsx): import createNewSessionName function to dynamically set a new session name when no session is visible * [autofix.ci] apply automated fixes * ✨ (monitor.py): rename get_sessions endpoint to get_message_sessions for clarity and consistency 🔧 (constants.ts): remove unused SESSIONS constant from API URLs 🔧 (use-delete-messages.ts): remove commented out code and unnecessary comments ✨ (use-delete-sessions.ts): add functionality to delete sessions in frontend 🔧 (use-get-sessions-from-flow.ts): update API endpoint for getting sessions to match backend changes 🔧 (playground-modal.tsx): add functionality to delete sessions and associated messages in the UI, update UI optimistically, and handle errors appropriately * [autofix.ci] apply automated fixes * 🐛 (monitor.py): Fix type hinting issue in delete_messages function 📝 (monitor.py): Add comments and improve readability in test_messages_endpoints.py 📝 (session_endpoint.py): Update endpoint paths for consistency and clarity in test_session_endpoint.py * [autofix.ci] apply automated fixes * fix: update SQL statement to use col() for session_id filtering in get_message_sessions function --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
068db12c33
commit
043ba55718
19 changed files with 672 additions and 65 deletions
|
|
@ -233,7 +233,7 @@ ignore-regex = '.*(Stati Uniti|Tense=Pres).*'
|
||||||
timeout = 120
|
timeout = 120
|
||||||
timeout_method = "signal"
|
timeout_method = "signal"
|
||||||
minversion = "6.0"
|
minversion = "6.0"
|
||||||
testpaths = ["tests", "integration"]
|
testpaths = ["src/backend/tests"]
|
||||||
console_output_style = "progress"
|
console_output_style = "progress"
|
||||||
filterwarnings = ["ignore::DeprecationWarning", "ignore::ResourceWarning"]
|
filterwarnings = ["ignore::DeprecationWarning", "ignore::ResourceWarning"]
|
||||||
log_cli = true
|
log_cli = true
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,24 @@ async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSes
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/messages/sessions", dependencies=[Depends(get_current_active_user)])
|
||||||
|
async def get_message_sessions(
|
||||||
|
session: DbSession,
|
||||||
|
flow_id: Annotated[UUID | None, Query()] = None,
|
||||||
|
) -> list[str]:
|
||||||
|
try:
|
||||||
|
stmt = select(MessageTable.session_id).distinct()
|
||||||
|
stmt = stmt.where(col(MessageTable.session_id).isnot(None))
|
||||||
|
|
||||||
|
if flow_id:
|
||||||
|
stmt = stmt.where(MessageTable.flow_id == flow_id)
|
||||||
|
|
||||||
|
session_ids = await session.exec(stmt)
|
||||||
|
return list(session_ids)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@router.get("/messages")
|
@router.get("/messages")
|
||||||
async def get_messages(
|
async def get_messages(
|
||||||
session: DbSession,
|
session: DbSession,
|
||||||
|
|
@ -54,7 +72,10 @@ async def get_messages(
|
||||||
if flow_id:
|
if flow_id:
|
||||||
stmt = stmt.where(MessageTable.flow_id == flow_id)
|
stmt = stmt.where(MessageTable.flow_id == flow_id)
|
||||||
if session_id:
|
if session_id:
|
||||||
stmt = stmt.where(MessageTable.session_id == session_id)
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
decoded_session_id = unquote(session_id)
|
||||||
|
stmt = stmt.where(MessageTable.session_id == decoded_session_id)
|
||||||
if sender:
|
if sender:
|
||||||
stmt = stmt.where(MessageTable.sender == sender)
|
stmt = stmt.where(MessageTable.sender == sender)
|
||||||
if sender_name:
|
if sender_name:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from urllib.parse import quote
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -32,6 +33,20 @@ async def created_messages(session): # noqa: ARG001
|
||||||
return await aadd_messagetables(messagetables, _session)
|
return await aadd_messagetables(messagetables, _session)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def messages_with_datetime_session_id(session): # noqa: ARG001
|
||||||
|
"""Create messages with datetime-like session IDs that contain characters requiring URL encoding."""
|
||||||
|
datetime_session_id = "2024-01-15 10:30:45 UTC" # Contains spaces and colons
|
||||||
|
async with session_scope() as _session:
|
||||||
|
messages = [
|
||||||
|
MessageCreate(text="Datetime message 1", sender="User", sender_name="User", session_id=datetime_session_id),
|
||||||
|
MessageCreate(text="Datetime message 2", sender="AI", sender_name="AI", session_id=datetime_session_id),
|
||||||
|
]
|
||||||
|
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
|
||||||
|
created_messages = await aadd_messagetables(messagetables, _session)
|
||||||
|
return created_messages, datetime_session_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.api_key_required
|
@pytest.mark.api_key_required
|
||||||
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
|
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
|
||||||
response = await client.request(
|
response = await client.request(
|
||||||
|
|
@ -127,3 +142,96 @@ async def test_no_messages_found_with_given_session_id(client, logged_in_headers
|
||||||
|
|
||||||
assert response.status_code == 404, response.text
|
assert response.status_code == 404, response.text
|
||||||
assert response.json()["detail"] == "Not Found"
|
assert response.json()["detail"] == "Not Found"
|
||||||
|
|
||||||
|
|
||||||
|
# Test for URL-encoded datetime session ID
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_messages_with_url_encoded_datetime_session_id(
|
||||||
|
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
|
||||||
|
):
|
||||||
|
"""Test that URL-encoded datetime session IDs are properly decoded and matched."""
|
||||||
|
created_messages, datetime_session_id = messages_with_datetime_session_id
|
||||||
|
|
||||||
|
# URL encode the datetime session ID (spaces become %20, colons become %3A)
|
||||||
|
encoded_session_id = quote(datetime_session_id)
|
||||||
|
|
||||||
|
# Test with URL-encoded session ID
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
messages = response.json()
|
||||||
|
assert len(messages) == 2
|
||||||
|
|
||||||
|
# Verify all messages have the correct (decoded) session ID
|
||||||
|
for message in messages:
|
||||||
|
assert message["session_id"] == datetime_session_id
|
||||||
|
|
||||||
|
# Verify message content
|
||||||
|
assert messages[0]["text"] == "Datetime message 1"
|
||||||
|
assert messages[1]["text"] == "Datetime message 2"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_messages_with_non_encoded_datetime_session_id(
|
||||||
|
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
|
||||||
|
):
|
||||||
|
"""Test that non-URL-encoded datetime session IDs also work correctly."""
|
||||||
|
created_messages, datetime_session_id = messages_with_datetime_session_id
|
||||||
|
|
||||||
|
# Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings)
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
messages = response.json()
|
||||||
|
assert len(messages) == 2
|
||||||
|
|
||||||
|
# Verify all messages have the correct session ID
|
||||||
|
for message in messages:
|
||||||
|
assert message["session_id"] == datetime_session_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_messages_with_various_encoded_characters(client: AsyncClient, logged_in_headers):
|
||||||
|
"""Test various URL-encoded characters in session IDs."""
|
||||||
|
# Create a session ID with various special characters
|
||||||
|
special_session_id = "test+session:2024@domain.com"
|
||||||
|
|
||||||
|
async with session_scope() as session:
|
||||||
|
message = MessageCreate(
|
||||||
|
text="Special chars message", sender="User", sender_name="User", session_id=special_session_id
|
||||||
|
)
|
||||||
|
messagetable = MessageTable.model_validate(message, from_attributes=True)
|
||||||
|
await aadd_messagetables([messagetable], session)
|
||||||
|
|
||||||
|
# URL encode the session ID
|
||||||
|
encoded_session_id = quote(special_session_id)
|
||||||
|
|
||||||
|
# Test with URL-encoded session ID
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
messages = response.json()
|
||||||
|
assert len(messages) == 1
|
||||||
|
assert messages[0]["session_id"] == special_session_id
|
||||||
|
assert messages[0]["text"] == "Special chars message"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_messages_empty_result_with_encoded_nonexistent_session(client: AsyncClient, logged_in_headers):
|
||||||
|
"""Test that URL-encoded non-existent session IDs return empty results."""
|
||||||
|
nonexistent_session_id = "2024-12-31 23:59:59 UTC"
|
||||||
|
encoded_session_id = quote(nonexistent_session_id)
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
messages = response.json()
|
||||||
|
assert len(messages) == 0
|
||||||
|
|
|
||||||
142
src/backend/tests/unit/test_session_endpoint.py
Normal file
142
src/backend/tests/unit/test_session_endpoint.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from langflow.memory import aadd_messagetables
|
||||||
|
from langflow.services.database.models.message.model import MessageTable
|
||||||
|
from langflow.services.deps import session_scope
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def messages_with_flow_ids(session): # noqa: ARG001
|
||||||
|
"""Create messages with different session_ids and flow_ids for testing sessions endpoint."""
|
||||||
|
async with session_scope() as _session:
|
||||||
|
flow_id_1 = uuid4()
|
||||||
|
flow_id_2 = uuid4()
|
||||||
|
|
||||||
|
# Create MessageTable objects directly since MessageCreate doesn't have flow_id field
|
||||||
|
messagetables = [
|
||||||
|
MessageTable(
|
||||||
|
text="Message 1", sender="User", sender_name="User", session_id="session_A", flow_id=flow_id_1
|
||||||
|
),
|
||||||
|
MessageTable(text="Message 2", sender="AI", sender_name="AI", session_id="session_A", flow_id=flow_id_1),
|
||||||
|
MessageTable(
|
||||||
|
text="Message 3", sender="User", sender_name="User", session_id="session_B", flow_id=flow_id_1
|
||||||
|
),
|
||||||
|
MessageTable(
|
||||||
|
text="Message 4", sender="User", sender_name="User", session_id="session_C", flow_id=flow_id_2
|
||||||
|
),
|
||||||
|
MessageTable(text="Message 5", sender="AI", sender_name="AI", session_id="session_D", flow_id=flow_id_2),
|
||||||
|
MessageTable(
|
||||||
|
text="Message 6",
|
||||||
|
sender="User",
|
||||||
|
sender_name="User",
|
||||||
|
session_id="session_E",
|
||||||
|
flow_id=None, # No flow_id
|
||||||
|
),
|
||||||
|
]
|
||||||
|
created_messages = await aadd_messagetables(messagetables, _session)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": created_messages,
|
||||||
|
"flow_id_1": flow_id_1,
|
||||||
|
"flow_id_2": flow_id_2,
|
||||||
|
"expected_sessions_flow_1": {"session_A", "session_B"},
|
||||||
|
"expected_sessions_flow_2": {"session_C", "session_D"},
|
||||||
|
"expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Tests for /sessions endpoint
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||||
|
"""Test getting all sessions without any filter."""
|
||||||
|
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
sessions = response.json()
|
||||||
|
assert isinstance(sessions, list)
|
||||||
|
|
||||||
|
# Convert to set for easier comparison since order doesn't matter
|
||||||
|
returned_sessions = set(sessions)
|
||||||
|
expected_sessions = messages_with_flow_ids["expected_all_sessions"]
|
||||||
|
|
||||||
|
assert returned_sessions == expected_sessions
|
||||||
|
assert len(sessions) == len(expected_sessions)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||||
|
"""Test getting sessions filtered by flow_id."""
|
||||||
|
flow_id_1 = messages_with_flow_ids["flow_id_1"]
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
sessions = response.json()
|
||||||
|
assert isinstance(sessions, list)
|
||||||
|
|
||||||
|
returned_sessions = set(sessions)
|
||||||
|
expected_sessions = messages_with_flow_ids["expected_sessions_flow_1"]
|
||||||
|
|
||||||
|
assert returned_sessions == expected_sessions
|
||||||
|
assert len(sessions) == len(expected_sessions)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
|
||||||
|
"""Test getting sessions filtered by a different flow_id."""
|
||||||
|
flow_id_2 = messages_with_flow_ids["flow_id_2"]
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
sessions = response.json()
|
||||||
|
assert isinstance(sessions, list)
|
||||||
|
|
||||||
|
returned_sessions = set(sessions)
|
||||||
|
expected_sessions = messages_with_flow_ids["expected_sessions_flow_2"]
|
||||||
|
|
||||||
|
assert returned_sessions == expected_sessions
|
||||||
|
assert len(sessions) == len(expected_sessions)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers):
|
||||||
|
"""Test getting sessions with a non-existent flow_id returns empty list."""
|
||||||
|
non_existent_flow_id = uuid4()
|
||||||
|
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
sessions = response.json()
|
||||||
|
assert isinstance(sessions, list)
|
||||||
|
assert len(sessions) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers):
|
||||||
|
"""Test getting sessions when no messages exist in database."""
|
||||||
|
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
sessions = response.json()
|
||||||
|
assert isinstance(sessions, list)
|
||||||
|
assert len(sessions) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.api_key_required
|
||||||
|
async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers):
|
||||||
|
"""Test getting sessions with invalid flow_id format returns 422."""
|
||||||
|
response = await client.get(
|
||||||
|
"api/v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 422, response.text
|
||||||
|
assert "detail" in response.json()
|
||||||
|
|
@ -12,7 +12,7 @@ export const useDeleteMessages: useMutationFunctionType<
|
||||||
undefined,
|
undefined,
|
||||||
DeleteMessagesParams
|
DeleteMessagesParams
|
||||||
> = (options?) => {
|
> = (options?) => {
|
||||||
const { mutate } = UseRequestProcessor();
|
const { mutate, queryClient } = UseRequestProcessor();
|
||||||
|
|
||||||
const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise<any> => {
|
const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise<any> => {
|
||||||
const response = await api.delete(`${getURL("MESSAGES")}`, {
|
const response = await api.delete(`${getURL("MESSAGES")}`, {
|
||||||
|
|
@ -26,7 +26,15 @@ export const useDeleteMessages: useMutationFunctionType<
|
||||||
DeleteMessagesParams,
|
DeleteMessagesParams,
|
||||||
any,
|
any,
|
||||||
DeleteMessagesParams
|
DeleteMessagesParams
|
||||||
> = mutate(["useDeleteMessages"], deleteMessage, options);
|
> = mutate(["useDeleteMessages"], deleteMessage, {
|
||||||
|
...options,
|
||||||
|
onSettled: (data, error, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["useGetSessionsFromFlowQuery"],
|
||||||
|
});
|
||||||
|
options?.onSettled?.(data, error, variables, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return mutation;
|
return mutation;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { useMutationFunctionType } from "@/types/api";
|
||||||
|
import { UseMutationResult } from "@tanstack/react-query";
|
||||||
|
import { api } from "../../api";
|
||||||
|
import { getURL } from "../../helpers/constants";
|
||||||
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
||||||
|
interface DeleteSessionParams {
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteSession: useMutationFunctionType<
|
||||||
|
undefined,
|
||||||
|
DeleteSessionParams
|
||||||
|
> = (options?) => {
|
||||||
|
const { mutate, queryClient } = UseRequestProcessor();
|
||||||
|
|
||||||
|
const deleteSession = async ({
|
||||||
|
sessionId,
|
||||||
|
}: DeleteSessionParams): Promise<any> => {
|
||||||
|
const response = await api.delete(
|
||||||
|
`${getURL("MESSAGES")}/session/${sessionId}`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutation: UseMutationResult<
|
||||||
|
DeleteSessionParams,
|
||||||
|
any,
|
||||||
|
DeleteSessionParams
|
||||||
|
> = mutate(["useDeleteSession"], deleteSession, {
|
||||||
|
...options,
|
||||||
|
onSettled: (data, error, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["useGetSessionsFromFlowQuery"],
|
||||||
|
});
|
||||||
|
options?.onSettled?.(data, error, variables, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,10 @@ import { useMessagesStore } from "@/stores/messagesStore";
|
||||||
import { UseMutationResult } from "@tanstack/react-query";
|
import { UseMutationResult } from "@tanstack/react-query";
|
||||||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { extractColumnsFromRows } from "../../../../utils/utils";
|
import {
|
||||||
|
extractColumnsFromRows,
|
||||||
|
prepareSessionIdForAPI,
|
||||||
|
} from "../../../../utils/utils";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
@ -109,7 +112,14 @@ export const useGetMessagesPollingMutation = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
config["params"] = { ...config["params"], ...params };
|
// Process params to ensure session_id is properly encoded
|
||||||
|
const processedParams = { ...params } as any;
|
||||||
|
if (processedParams.session_id) {
|
||||||
|
processedParams.session_id = prepareSessionIdForAPI(
|
||||||
|
processedParams.session_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config["params"] = { ...config["params"], ...processedParams };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.get<any>(`${getURL("MESSAGES")}`, config);
|
const data = await api.get<any>(`${getURL("MESSAGES")}`, config);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ import { useMessagesStore } from "@/stores/messagesStore";
|
||||||
import { keepPreviousData } from "@tanstack/react-query";
|
import { keepPreviousData } from "@tanstack/react-query";
|
||||||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||||
import { useQueryFunctionType } from "../../../../types/api";
|
import { useQueryFunctionType } from "../../../../types/api";
|
||||||
import { extractColumnsFromRows } from "../../../../utils/utils";
|
import {
|
||||||
|
extractColumnsFromRows,
|
||||||
|
prepareSessionIdForAPI,
|
||||||
|
} from "../../../../utils/utils";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { getURL } from "../../helpers/constants";
|
import { getURL } from "../../helpers/constants";
|
||||||
import { UseRequestProcessor } from "../../services/request-processor";
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
@ -33,7 +36,14 @@ export const useGetMessagesQuery: useQueryFunctionType<
|
||||||
config["params"] = { flow_id: id };
|
config["params"] = { flow_id: id };
|
||||||
}
|
}
|
||||||
if (params) {
|
if (params) {
|
||||||
config["params"] = { ...config["params"], ...params };
|
// Process params to ensure session_id is properly encoded
|
||||||
|
const processedParams = { ...params } as any;
|
||||||
|
if (processedParams.session_id) {
|
||||||
|
processedParams.session_id = prepareSessionIdForAPI(
|
||||||
|
processedParams.session_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config["params"] = { ...config["params"], ...processedParams };
|
||||||
}
|
}
|
||||||
if (!isPlaygroundPage) {
|
if (!isPlaygroundPage) {
|
||||||
return await api.get<any>(`${getURL("MESSAGES")}`, config);
|
return await api.get<any>(`${getURL("MESSAGES")}`, config);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
|
import { keepPreviousData } from "@tanstack/react-query";
|
||||||
|
import { useQueryFunctionType } from "../../../../types/api";
|
||||||
|
import { api } from "../../api";
|
||||||
|
import { getURL } from "../../helpers/constants";
|
||||||
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
||||||
|
interface SessionsQueryParams {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionsResponse {
|
||||||
|
sessions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetSessionsFromFlowQuery: useQueryFunctionType<
|
||||||
|
SessionsQueryParams,
|
||||||
|
SessionsResponse
|
||||||
|
> = ({ id }, options) => {
|
||||||
|
const { query } = UseRequestProcessor();
|
||||||
|
|
||||||
|
const getSessionsFn = async (id?: string) => {
|
||||||
|
const isPlaygroundPage = useFlowStore.getState().playgroundPage;
|
||||||
|
const config = {};
|
||||||
|
if (id) {
|
||||||
|
config["params"] = { flow_id: id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlaygroundPage) {
|
||||||
|
return await api.get<string[]>(`${getURL("MESSAGES")}/sessions`, config);
|
||||||
|
} else {
|
||||||
|
// For playground mode, get sessions from sessionStorage
|
||||||
|
const data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]");
|
||||||
|
// Extract unique session IDs from stored messages
|
||||||
|
const sessionIdsSet = new Set(
|
||||||
|
data.map((msg: any) => msg.session_id).filter(Boolean),
|
||||||
|
);
|
||||||
|
const sessionIds = Array.from(sessionIdsSet);
|
||||||
|
|
||||||
|
// Always include the flow ID as the default session if it's not already present
|
||||||
|
if (id && !sessionIds.includes(id)) {
|
||||||
|
sessionIds.unshift(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: sessionIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseFn = async () => {
|
||||||
|
const response = await getSessionsFn(id);
|
||||||
|
return { sessions: response.data };
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryResult = query(
|
||||||
|
["useGetSessionsFromFlowQuery", { id }],
|
||||||
|
responseFn,
|
||||||
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return queryResult;
|
||||||
|
};
|
||||||
|
|
@ -48,10 +48,9 @@ export const useUpdateSessionName: useMutationFunctionType<
|
||||||
const mutation: UseMutationResult<Message[], any, UpdateSessionParams> =
|
const mutation: UseMutationResult<Message[], any, UpdateSessionParams> =
|
||||||
mutate(["useUpdateSessionName"], updateSessionApi, {
|
mutate(["useUpdateSessionName"], updateSessionApi, {
|
||||||
...options,
|
...options,
|
||||||
onSettled: (data, variables, context) => {
|
onSettled: () => {
|
||||||
// Invalidate and refetch relevant queries
|
queryClient.invalidateQueries({
|
||||||
queryClient.refetchQueries({
|
queryKey: ["useGetSessionsFromFlowQuery"],
|
||||||
queryKey: ["useGetMessagesQuery"],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import IOModal from "@/modals/IOModal/new-modal";
|
import IOModal from "@/modals/IOModal/playground-modal";
|
||||||
import { IOModalPropsType } from "@/types/components";
|
import { IOModalPropsType } from "@/types/components";
|
||||||
|
|
||||||
export function CustomIOModal({
|
export function CustomIOModal({
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export default function SessionSelector({
|
||||||
selectedView,
|
selectedView,
|
||||||
setSelectedView,
|
setSelectedView,
|
||||||
playgroundPage,
|
playgroundPage,
|
||||||
|
setActiveSession,
|
||||||
}: {
|
}: {
|
||||||
deleteSession: (session: string) => void;
|
deleteSession: (session: string) => void;
|
||||||
session: string;
|
session: string;
|
||||||
|
|
@ -36,6 +37,7 @@ export default function SessionSelector({
|
||||||
selectedView?: { type: string; id: string };
|
selectedView?: { type: string; id: string };
|
||||||
setSelectedView: (view: { type: string; id: string } | undefined) => void;
|
setSelectedView: (view: { type: string; id: string } | undefined) => void;
|
||||||
playgroundPage: boolean;
|
playgroundPage: boolean;
|
||||||
|
setActiveSession: (session: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const clientId = useUtilityStore((state) => state.clientId);
|
const clientId = useUtilityStore((state) => state.clientId);
|
||||||
let realFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
let realFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||||
|
|
@ -46,6 +48,9 @@ export default function SessionSelector({
|
||||||
const [editedSession, setEditedSession] = useState(session);
|
const [editedSession, setEditedSession] = useState(session);
|
||||||
const { mutate: updateSessionName } = useUpdateSessionName();
|
const { mutate: updateSessionName } = useUpdateSessionName();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const setNewChatOnPlayground = useFlowStore(
|
||||||
|
(state) => state.setNewChatOnPlayground,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditedSession(session);
|
setEditedSession(session);
|
||||||
|
|
@ -68,13 +73,13 @@ export default function SessionSelector({
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
updateVisibleSession(editedSession);
|
updateVisibleSession(editedSession.trim());
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
selectedView?.type === "Session" &&
|
selectedView?.type === "Session" &&
|
||||||
selectedView?.id === session
|
selectedView?.id === session
|
||||||
) {
|
) {
|
||||||
setSelectedView({ type: "Session", id: editedSession });
|
setSelectedView({ type: "Session", id: editedSession.trim() });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const createNewSessionName = () => {
|
||||||
|
return `Session ${new Date().toLocaleString("en-US", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC",
|
||||||
|
})}`;
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { useVoiceStore } from "@/stores/voiceStore";
|
import { useVoiceStore } from "@/stores/voiceStore";
|
||||||
import IconComponent from "../../../components/common/genericIconComponent";
|
import IconComponent from "../../../components/common/genericIconComponent";
|
||||||
import { SidebarOpenViewProps } from "../types/sidebar-open-view";
|
import { SidebarOpenViewProps } from "../types/sidebar-open-view";
|
||||||
|
|
@ -13,11 +14,16 @@ export const SidebarOpenView = ({
|
||||||
visibleSession,
|
visibleSession,
|
||||||
selectedViewField,
|
selectedViewField,
|
||||||
playgroundPage,
|
playgroundPage,
|
||||||
|
setActiveSession,
|
||||||
}: SidebarOpenViewProps) => {
|
}: SidebarOpenViewProps) => {
|
||||||
const setNewSessionCloseVoiceAssistant = useVoiceStore(
|
const setNewSessionCloseVoiceAssistant = useVoiceStore(
|
||||||
(state) => state.setNewSessionCloseVoiceAssistant,
|
(state) => state.setNewSessionCloseVoiceAssistant,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setNewChatOnPlayground = useFlowStore(
|
||||||
|
(state) => state.setNewChatOnPlayground,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col pl-3">
|
<div className="flex flex-col pl-3">
|
||||||
|
|
@ -40,6 +46,7 @@ export const SidebarOpenView = ({
|
||||||
setvisibleSession(undefined);
|
setvisibleSession(undefined);
|
||||||
setSelectedViewField(undefined);
|
setSelectedViewField(undefined);
|
||||||
setNewSessionCloseVoiceAssistant(true);
|
setNewSessionCloseVoiceAssistant(true);
|
||||||
|
setNewChatOnPlayground(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
|
|
@ -78,6 +85,9 @@ export const SidebarOpenView = ({
|
||||||
type: "Session",
|
type: "Session",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
setActiveSession={(session) => {
|
||||||
|
setActiveSession(session);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react";
|
//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react";
|
||||||
import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons";
|
import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons";
|
||||||
import { EventDeliveryType } from "@/constants/enums";
|
|
||||||
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
|
|
||||||
import {
|
import {
|
||||||
useDeleteMessages,
|
useDeleteMessages,
|
||||||
useGetMessagesQuery,
|
useGetMessagesQuery,
|
||||||
} from "@/controllers/API/queries/messages";
|
} from "@/controllers/API/queries/messages";
|
||||||
|
import { useDeleteSession } from "@/controllers/API/queries/messages/use-delete-sessions";
|
||||||
|
import { useGetSessionsFromFlowQuery } from "@/controllers/API/queries/messages/use-get-sessions-from-flow";
|
||||||
import { ENABLE_PUBLISH } from "@/customization/feature-flags";
|
import { ENABLE_PUBLISH } from "@/customization/feature-flags";
|
||||||
import { track } from "@/customization/utils/analytics";
|
import { track } from "@/customization/utils/analytics";
|
||||||
import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab";
|
import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab";
|
||||||
import { LangflowButtonRedirectTarget } from "@/customization/utils/urls";
|
import { LangflowButtonRedirectTarget } from "@/customization/utils/urls";
|
||||||
import { useUtilityStore } from "@/stores/utilityStore";
|
import { useUtilityStore } from "@/stores/utilityStore";
|
||||||
import { swatchColors } from "@/utils/styleUtils";
|
import { swatchColors } from "@/utils/styleUtils";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react";
|
import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react";
|
||||||
|
|
@ -27,6 +27,7 @@ import { IOModalPropsType } from "../../types/components";
|
||||||
import { cn, getNumberFromString } from "../../utils/utils";
|
import { cn, getNumberFromString } from "../../utils/utils";
|
||||||
import BaseModal from "../baseModal";
|
import BaseModal from "../baseModal";
|
||||||
import { ChatViewWrapper } from "./components/chat-view-wrapper";
|
import { ChatViewWrapper } from "./components/chat-view-wrapper";
|
||||||
|
import { createNewSessionName } from "./components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name";
|
||||||
import { SelectedViewField } from "./components/selected-view-field";
|
import { SelectedViewField } from "./components/selected-view-field";
|
||||||
import { SidebarOpenView } from "./components/sidebar-open-view";
|
import { SidebarOpenView } from "./components/sidebar-open-view";
|
||||||
|
|
||||||
|
|
@ -46,6 +47,13 @@ export default function IOModal({
|
||||||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||||
|
const newChatOnPlayground = useFlowStore(
|
||||||
|
(state) => state.newChatOnPlayground,
|
||||||
|
);
|
||||||
|
const setNewChatOnPlayground = useFlowStore(
|
||||||
|
(state) => state.setNewChatOnPlayground,
|
||||||
|
);
|
||||||
|
|
||||||
const { flowIcon, flowId, flowGradient, flowName } = useFlowStore(
|
const { flowIcon, flowId, flowGradient, flowName } = useFlowStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
flowIcon: state.currentFlow?.icon,
|
flowIcon: state.currentFlow?.icon,
|
||||||
|
|
@ -76,12 +84,36 @@ export default function IOModal({
|
||||||
: realFlowId;
|
: realFlowId;
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
const { mutate: deleteSessionFunction } = useDeleteMessages();
|
const { mutate: deleteMessagesFunction } = useDeleteMessages();
|
||||||
|
const { mutate: deleteSessionFunction } = useDeleteSession();
|
||||||
|
|
||||||
const [visibleSession, setvisibleSession] = useState<string | undefined>(
|
const [visibleSession, setvisibleSession] = useState<string | undefined>(
|
||||||
currentFlowId,
|
currentFlowId,
|
||||||
);
|
);
|
||||||
const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground";
|
const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground";
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: sessionsFromDb,
|
||||||
|
isLoading: sessionsLoading,
|
||||||
|
refetch: refetchSessions,
|
||||||
|
} = useGetSessionsFromFlowQuery(
|
||||||
|
{
|
||||||
|
id: currentFlowId,
|
||||||
|
},
|
||||||
|
{ enabled: open },
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionsFromDb && !sessionsLoading) {
|
||||||
|
const sessions = [...sessionsFromDb.sessions];
|
||||||
|
// Always include the currentFlowId as the default session if it's not already present
|
||||||
|
if (!sessions.includes(currentFlowId)) {
|
||||||
|
sessions.unshift(currentFlowId);
|
||||||
|
}
|
||||||
|
setSessions(sessions);
|
||||||
|
}
|
||||||
|
}, [sessionsFromDb, sessionsLoading, currentFlowId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIOModalOpen(open);
|
setIOModalOpen(open);
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -90,25 +122,45 @@ export default function IOModal({
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
function handleDeleteSession(session_id: string) {
|
function handleDeleteSession(session_id: string) {
|
||||||
|
// Update UI optimistically
|
||||||
|
if (visibleSession === session_id) {
|
||||||
|
const remainingSessions = sessions.filter((s) => s !== session_id);
|
||||||
|
if (remainingSessions.length > 0) {
|
||||||
|
setvisibleSession(remainingSessions[0]);
|
||||||
|
} else {
|
||||||
|
setvisibleSession(currentFlowId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the session (which will delete all associated messages on the backend)
|
||||||
deleteSessionFunction(
|
deleteSessionFunction(
|
||||||
{
|
{ sessionId: session_id },
|
||||||
ids: messages
|
|
||||||
.filter((msg) => msg.session_id === session_id)
|
|
||||||
.map((msg) => msg.id),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
// Remove the session from local state
|
||||||
|
deleteSession(session_id);
|
||||||
|
|
||||||
|
// Remove all messages for this session from local state
|
||||||
|
const messageIdsToRemove = messages
|
||||||
|
.filter((msg) => msg.session_id === session_id)
|
||||||
|
.map((msg) => msg.id);
|
||||||
|
|
||||||
|
if (messageIdsToRemove.length > 0) {
|
||||||
|
removeMessages(messageIdsToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
setSuccessData({
|
setSuccessData({
|
||||||
title: "Session deleted successfully.",
|
title: "Session deleted successfully.",
|
||||||
});
|
});
|
||||||
deleteSession(session_id);
|
|
||||||
if (visibleSession === session_id) {
|
|
||||||
setvisibleSession(undefined);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
// Revert optimistic UI update on error
|
||||||
|
if (visibleSession !== session_id) {
|
||||||
|
setvisibleSession(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: "Error deleting Session.",
|
title: "Error deleting session.",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -132,27 +184,24 @@ export default function IOModal({
|
||||||
>(startView());
|
>(startView());
|
||||||
|
|
||||||
const messages = useMessagesStore((state) => state.messages);
|
const messages = useMessagesStore((state) => state.messages);
|
||||||
const [sessions, setSessions] = useState<string[]>(
|
const removeMessages = useMessagesStore((state) => state.removeMessages);
|
||||||
Array.from(
|
const [sessions, setSessions] = useState<string[]>([]);
|
||||||
new Set(
|
|
||||||
messages
|
|
||||||
.filter((message) => message.flow_id === currentFlowId)
|
|
||||||
.map((message) => message.session_id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const [sessionId, setSessionId] = useState<string>(currentFlowId);
|
const [sessionId, setSessionId] = useState<string>(currentFlowId);
|
||||||
const setCurrentSessionId = useUtilityStore(
|
const setCurrentSessionId = useUtilityStore(
|
||||||
(state) => state.setCurrentSessionId,
|
(state) => state.setCurrentSessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isFetched: messagesFetched } = useGetMessagesQuery(
|
const { isFetched: messagesFetched, refetch: refetchMessages } =
|
||||||
{
|
useGetMessagesQuery(
|
||||||
mode: "union",
|
{
|
||||||
id: currentFlowId,
|
mode: "union",
|
||||||
},
|
id: currentFlowId,
|
||||||
{ enabled: open },
|
params: {
|
||||||
);
|
session_id: visibleSession,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ enabled: open },
|
||||||
|
);
|
||||||
|
|
||||||
const chatValue = useUtilityStore((state) => state.chatValueStore);
|
const chatValue = useUtilityStore((state) => state.chatValueStore);
|
||||||
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
|
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
|
||||||
|
|
@ -185,28 +234,28 @@ export default function IOModal({
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sessions = new Set<string>();
|
if (newChatOnPlayground && !sessionsLoading) {
|
||||||
messages
|
const handleRefetchAndSetSession = async () => {
|
||||||
.filter((message) => message.flow_id === currentFlowId)
|
try {
|
||||||
.forEach((row) => {
|
const result = await refetchSessions();
|
||||||
sessions.add(row.session_id);
|
if (result.data?.sessions && result.data.sessions.length > 0) {
|
||||||
});
|
setvisibleSession(
|
||||||
setSessions((prev) => {
|
result.data.sessions[result.data.sessions.length - 1],
|
||||||
if (prev.length < Array.from(sessions).length) {
|
);
|
||||||
// set the new session as visible
|
}
|
||||||
setvisibleSession(
|
} catch (error) {
|
||||||
Array.from(sessions)[Array.from(sessions).length - 1],
|
console.error("Error refetching sessions:", error);
|
||||||
);
|
}
|
||||||
}
|
};
|
||||||
return Array.from(sessions);
|
|
||||||
});
|
handleRefetchAndSetSession();
|
||||||
|
setNewChatOnPlayground(false);
|
||||||
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visibleSession) {
|
if (!visibleSession) {
|
||||||
setSessionId(
|
setSessionId(createNewSessionName());
|
||||||
`Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`,
|
|
||||||
);
|
|
||||||
setCurrentSessionId(currentFlowId);
|
setCurrentSessionId(currentFlowId);
|
||||||
} else if (visibleSession) {
|
} else if (visibleSession) {
|
||||||
setSessionId(visibleSession);
|
setSessionId(visibleSession);
|
||||||
|
|
@ -271,6 +320,35 @@ export default function IOModal({
|
||||||
: getNumberFromString(flowGradient ?? flowId ?? "")) %
|
: getNumberFromString(flowGradient ?? flowId ?? "")) %
|
||||||
swatchColors.length;
|
swatchColors.length;
|
||||||
|
|
||||||
|
const setActiveSession = (session: string) => {
|
||||||
|
setvisibleSession((prev) => {
|
||||||
|
if (prev === session) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
|
const prevVisibleSessionRef = useRef<string | undefined>(visibleSession);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasInitialized) {
|
||||||
|
setHasInitialized(true);
|
||||||
|
prevVisibleSessionRef.current = visibleSession;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
open &&
|
||||||
|
visibleSession &&
|
||||||
|
prevVisibleSessionRef.current !== visibleSession
|
||||||
|
) {
|
||||||
|
refetchMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevVisibleSessionRef.current = visibleSession;
|
||||||
|
}, [visibleSession]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
open={open}
|
open={open}
|
||||||
|
|
@ -336,7 +414,7 @@ export default function IOModal({
|
||||||
</Button>
|
</Button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</div>
|
</div>
|
||||||
{sidebarOpen && (
|
{sidebarOpen && !sessionsLoading && (
|
||||||
<SidebarOpenView
|
<SidebarOpenView
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
setSelectedViewField={setSelectedViewField}
|
setSelectedViewField={setSelectedViewField}
|
||||||
|
|
@ -345,6 +423,7 @@ export default function IOModal({
|
||||||
visibleSession={visibleSession}
|
visibleSession={visibleSession}
|
||||||
selectedViewField={selectedViewField}
|
selectedViewField={selectedViewField}
|
||||||
playgroundPage={!!playgroundPage}
|
playgroundPage={!!playgroundPage}
|
||||||
|
setActiveSession={setActiveSession}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sidebarOpen && showPublishOptions && (
|
{sidebarOpen && showPublishOptions && (
|
||||||
|
|
@ -383,7 +462,7 @@ export default function IOModal({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex h-full min-w-96 flex-grow bg-background">
|
<div className="flex h-full min-w-96 flex-grow bg-background">
|
||||||
{selectedViewField && (
|
{selectedViewField && !sessionsLoading && (
|
||||||
<SelectedViewField
|
<SelectedViewField
|
||||||
selectedViewField={selectedViewField}
|
selectedViewField={selectedViewField}
|
||||||
setSelectedViewField={setSelectedViewField}
|
setSelectedViewField={setSelectedViewField}
|
||||||
|
|
@ -8,4 +8,5 @@ export type SidebarOpenViewProps = {
|
||||||
visibleSession: string | undefined;
|
visibleSession: string | undefined;
|
||||||
selectedViewField: { type: string; id: string } | undefined;
|
selectedViewField: { type: string; id: string } | undefined;
|
||||||
playgroundPage: boolean;
|
playgroundPage: boolean;
|
||||||
|
setActiveSession: (session: string) => void;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1061,6 +1061,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
);
|
);
|
||||||
set({ dismissedNodes: newDismissedNodes });
|
set({ dismissedNodes: newDismissedNodes });
|
||||||
},
|
},
|
||||||
|
setNewChatOnPlayground: (newChat: boolean) => {
|
||||||
|
set({ newChatOnPlayground: newChat });
|
||||||
|
},
|
||||||
|
newChatOnPlayground: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useFlowStore;
|
export default useFlowStore;
|
||||||
|
|
|
||||||
|
|
@ -285,4 +285,6 @@ export type FlowStoreType = {
|
||||||
setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void;
|
setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void;
|
||||||
clearEdgesRunningByNodes: () => Promise<void>;
|
clearEdgesRunningByNodes: () => Promise<void>;
|
||||||
updateToolMode: (nodeId: string, toolMode: boolean) => void;
|
updateToolMode: (nodeId: string, toolMode: boolean) => void;
|
||||||
|
newChatOnPlayground: boolean;
|
||||||
|
setNewChatOnPlayground: (newChat: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -902,3 +902,93 @@ export function getOS() {
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a session ID for safe URL transmission
|
||||||
|
* Handles both UUID format and date-time format session IDs
|
||||||
|
* @param {string} session_id - The session ID to encode
|
||||||
|
* @returns {string} The URL-encoded session ID
|
||||||
|
*/
|
||||||
|
export function encodeSessionId(session_id: string): string {
|
||||||
|
if (!session_id) return "";
|
||||||
|
// Use encodeURIComponent to properly encode spaces, commas, colons, etc.
|
||||||
|
return encodeURIComponent(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a session ID from URL encoding
|
||||||
|
* @param {string} encoded_session_id - The URL-encoded session ID
|
||||||
|
* @returns {string} The decoded session ID
|
||||||
|
*/
|
||||||
|
export function decodeSessionId(encoded_session_id: string): string {
|
||||||
|
if (!encoded_session_id) return "";
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(encoded_session_id);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to decode session ID:", encoded_session_id, error);
|
||||||
|
return encoded_session_id; // Return as-is if decoding fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a string is a valid UUID format
|
||||||
|
* @param {string} str - The string to validate
|
||||||
|
* @returns {boolean} True if the string is a valid UUID format
|
||||||
|
*/
|
||||||
|
export function isUUID(str: string): boolean {
|
||||||
|
const uuidRegex =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
return uuidRegex.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a string is a date-time session format
|
||||||
|
* @param {string} str - The string to validate
|
||||||
|
* @returns {boolean} True if the string appears to be a date-time session format
|
||||||
|
*/
|
||||||
|
export function isDateTimeSession(str: string): boolean {
|
||||||
|
// Check for patterns like "Session Jun 16, 15:44:08" or similar
|
||||||
|
const dateTimeSessionRegex =
|
||||||
|
/^Session\s+\w{3}\s+\d{1,2},\s+\d{2}:\d{2}:\d{2}$/;
|
||||||
|
return dateTimeSessionRegex.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats and normalizes session IDs for consistent handling
|
||||||
|
* Handles both UUID format and date-time format session IDs
|
||||||
|
* @param {string} session_id - The session ID to format
|
||||||
|
* @returns {string} The formatted session ID
|
||||||
|
*/
|
||||||
|
export function sessionIdFormatted(session_id: string): string {
|
||||||
|
if (!session_id) return "";
|
||||||
|
|
||||||
|
// Decode if it appears to be URL encoded
|
||||||
|
let decodedId = session_id;
|
||||||
|
if (session_id.includes("%") || session_id.includes("+")) {
|
||||||
|
decodedId = decodeSessionId(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a UUID, return as-is (already in good format)
|
||||||
|
if (isUUID(decodedId)) {
|
||||||
|
return decodedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a date-time session, return as-is
|
||||||
|
if (isDateTimeSession(decodedId)) {
|
||||||
|
return decodedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other format, return as-is but ensure it's properly trimmed
|
||||||
|
return decodedId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely prepares a session ID for API requests
|
||||||
|
* This function should be used when adding session_id to API parameters
|
||||||
|
* @param {string} session_id - The session ID to prepare
|
||||||
|
* @returns {string} The properly encoded session ID for API use
|
||||||
|
*/
|
||||||
|
export function prepareSessionIdForAPI(session_id: string): string {
|
||||||
|
const formatted = sessionIdFormatted(session_id);
|
||||||
|
return encodeSessionId(formatted);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue