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_method = "signal"
|
||||
minversion = "6.0"
|
||||
testpaths = ["tests", "integration"]
|
||||
testpaths = ["src/backend/tests"]
|
||||
console_output_style = "progress"
|
||||
filterwarnings = ["ignore::DeprecationWarning", "ignore::ResourceWarning"]
|
||||
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
|
||||
|
||||
|
||||
@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")
|
||||
async def get_messages(
|
||||
session: DbSession,
|
||||
|
|
@ -54,7 +72,10 @@ async def get_messages(
|
|||
if flow_id:
|
||||
stmt = stmt.where(MessageTable.flow_id == flow_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:
|
||||
stmt = stmt.where(MessageTable.sender == sender)
|
||||
if sender_name:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
|
@ -32,6 +33,20 @@ async def created_messages(session): # noqa: ARG001
|
|||
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
|
||||
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
|
||||
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.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,
|
||||
DeleteMessagesParams
|
||||
> = (options?) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
const { mutate, queryClient } = UseRequestProcessor();
|
||||
|
||||
const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise<any> => {
|
||||
const response = await api.delete(`${getURL("MESSAGES")}`, {
|
||||
|
|
@ -26,7 +26,15 @@ export const useDeleteMessages: useMutationFunctionType<
|
|||
DeleteMessagesParams,
|
||||
any,
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { extractColumnsFromRows } from "../../../../utils/utils";
|
||||
import {
|
||||
extractColumnsFromRows,
|
||||
prepareSessionIdForAPI,
|
||||
} from "../../../../utils/utils";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
|
@ -109,7 +112,14 @@ export const useGetMessagesPollingMutation = (
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { useMessagesStore } from "@/stores/messagesStore";
|
|||
import { keepPreviousData } from "@tanstack/react-query";
|
||||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { useQueryFunctionType } from "../../../../types/api";
|
||||
import { extractColumnsFromRows } from "../../../../utils/utils";
|
||||
import {
|
||||
extractColumnsFromRows,
|
||||
prepareSessionIdForAPI,
|
||||
} from "../../../../utils/utils";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
|
@ -33,7 +36,14 @@ export const useGetMessagesQuery: useQueryFunctionType<
|
|||
config["params"] = { flow_id: id };
|
||||
}
|
||||
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) {
|
||||
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> =
|
||||
mutate(["useUpdateSessionName"], updateSessionApi, {
|
||||
...options,
|
||||
onSettled: (data, variables, context) => {
|
||||
// Invalidate and refetch relevant queries
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetMessagesQuery"],
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["useGetSessionsFromFlowQuery"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import IOModal from "@/modals/IOModal/new-modal";
|
||||
import IOModal from "@/modals/IOModal/playground-modal";
|
||||
import { IOModalPropsType } from "@/types/components";
|
||||
|
||||
export function CustomIOModal({
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default function SessionSelector({
|
|||
selectedView,
|
||||
setSelectedView,
|
||||
playgroundPage,
|
||||
setActiveSession,
|
||||
}: {
|
||||
deleteSession: (session: string) => void;
|
||||
session: string;
|
||||
|
|
@ -36,6 +37,7 @@ export default function SessionSelector({
|
|||
selectedView?: { type: string; id: string };
|
||||
setSelectedView: (view: { type: string; id: string } | undefined) => void;
|
||||
playgroundPage: boolean;
|
||||
setActiveSession: (session: string) => void;
|
||||
}) {
|
||||
const clientId = useUtilityStore((state) => state.clientId);
|
||||
let realFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
|
|
@ -46,6 +48,9 @@ export default function SessionSelector({
|
|||
const [editedSession, setEditedSession] = useState(session);
|
||||
const { mutate: updateSessionName } = useUpdateSessionName();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const setNewChatOnPlayground = useFlowStore(
|
||||
(state) => state.setNewChatOnPlayground,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setEditedSession(session);
|
||||
|
|
@ -68,13 +73,13 @@ export default function SessionSelector({
|
|||
{
|
||||
onSuccess: () => {
|
||||
if (isVisible) {
|
||||
updateVisibleSession(editedSession);
|
||||
updateVisibleSession(editedSession.trim());
|
||||
}
|
||||
if (
|
||||
selectedView?.type === "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 { Button } from "@/components/ui/button";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useVoiceStore } from "@/stores/voiceStore";
|
||||
import IconComponent from "../../../components/common/genericIconComponent";
|
||||
import { SidebarOpenViewProps } from "../types/sidebar-open-view";
|
||||
|
|
@ -13,11 +14,16 @@ export const SidebarOpenView = ({
|
|||
visibleSession,
|
||||
selectedViewField,
|
||||
playgroundPage,
|
||||
setActiveSession,
|
||||
}: SidebarOpenViewProps) => {
|
||||
const setNewSessionCloseVoiceAssistant = useVoiceStore(
|
||||
(state) => state.setNewSessionCloseVoiceAssistant,
|
||||
);
|
||||
|
||||
const setNewChatOnPlayground = useFlowStore(
|
||||
(state) => state.setNewChatOnPlayground,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col pl-3">
|
||||
|
|
@ -40,6 +46,7 @@ export const SidebarOpenView = ({
|
|||
setvisibleSession(undefined);
|
||||
setSelectedViewField(undefined);
|
||||
setNewSessionCloseVoiceAssistant(true);
|
||||
setNewChatOnPlayground(true);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
|
|
@ -78,6 +85,9 @@ export const SidebarOpenView = ({
|
|||
type: "Session",
|
||||
});
|
||||
}}
|
||||
setActiveSession={(session) => {
|
||||
setActiveSession(session);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react";
|
||||
import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons";
|
||||
import { EventDeliveryType } from "@/constants/enums";
|
||||
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
|
||||
import {
|
||||
useDeleteMessages,
|
||||
useGetMessagesQuery,
|
||||
} 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 { track } from "@/customization/utils/analytics";
|
||||
import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab";
|
||||
import { LangflowButtonRedirectTarget } from "@/customization/utils/urls";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
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 { useShallow } from "zustand/react/shallow";
|
||||
import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react";
|
||||
|
|
@ -27,6 +27,7 @@ import { IOModalPropsType } from "../../types/components";
|
|||
import { cn, getNumberFromString } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
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 { SidebarOpenView } from "./components/sidebar-open-view";
|
||||
|
||||
|
|
@ -46,6 +47,13 @@ export default function IOModal({
|
|||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const newChatOnPlayground = useFlowStore(
|
||||
(state) => state.newChatOnPlayground,
|
||||
);
|
||||
const setNewChatOnPlayground = useFlowStore(
|
||||
(state) => state.setNewChatOnPlayground,
|
||||
);
|
||||
|
||||
const { flowIcon, flowId, flowGradient, flowName } = useFlowStore(
|
||||
useShallow((state) => ({
|
||||
flowIcon: state.currentFlow?.icon,
|
||||
|
|
@ -76,12 +84,36 @@ export default function IOModal({
|
|||
: realFlowId;
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
|
||||
const { mutate: deleteSessionFunction } = useDeleteMessages();
|
||||
const { mutate: deleteMessagesFunction } = useDeleteMessages();
|
||||
const { mutate: deleteSessionFunction } = useDeleteSession();
|
||||
|
||||
const [visibleSession, setvisibleSession] = useState<string | undefined>(
|
||||
currentFlowId,
|
||||
);
|
||||
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(() => {
|
||||
setIOModalOpen(open);
|
||||
return () => {
|
||||
|
|
@ -90,25 +122,45 @@ export default function IOModal({
|
|||
}, [open]);
|
||||
|
||||
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(
|
||||
{
|
||||
ids: messages
|
||||
.filter((msg) => msg.session_id === session_id)
|
||||
.map((msg) => msg.id),
|
||||
},
|
||||
{ sessionId: session_id },
|
||||
{
|
||||
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({
|
||||
title: "Session deleted successfully.",
|
||||
});
|
||||
deleteSession(session_id);
|
||||
if (visibleSession === session_id) {
|
||||
setvisibleSession(undefined);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
// Revert optimistic UI update on error
|
||||
if (visibleSession !== session_id) {
|
||||
setvisibleSession(session_id);
|
||||
}
|
||||
|
||||
setErrorData({
|
||||
title: "Error deleting Session.",
|
||||
title: "Error deleting session.",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -132,27 +184,24 @@ export default function IOModal({
|
|||
>(startView());
|
||||
|
||||
const messages = useMessagesStore((state) => state.messages);
|
||||
const [sessions, setSessions] = useState<string[]>(
|
||||
Array.from(
|
||||
new Set(
|
||||
messages
|
||||
.filter((message) => message.flow_id === currentFlowId)
|
||||
.map((message) => message.session_id),
|
||||
),
|
||||
),
|
||||
);
|
||||
const removeMessages = useMessagesStore((state) => state.removeMessages);
|
||||
const [sessions, setSessions] = useState<string[]>([]);
|
||||
const [sessionId, setSessionId] = useState<string>(currentFlowId);
|
||||
const setCurrentSessionId = useUtilityStore(
|
||||
(state) => state.setCurrentSessionId,
|
||||
);
|
||||
|
||||
const { isFetched: messagesFetched } = useGetMessagesQuery(
|
||||
{
|
||||
mode: "union",
|
||||
id: currentFlowId,
|
||||
},
|
||||
{ enabled: open },
|
||||
);
|
||||
const { isFetched: messagesFetched, refetch: refetchMessages } =
|
||||
useGetMessagesQuery(
|
||||
{
|
||||
mode: "union",
|
||||
id: currentFlowId,
|
||||
params: {
|
||||
session_id: visibleSession,
|
||||
},
|
||||
},
|
||||
{ enabled: open },
|
||||
);
|
||||
|
||||
const chatValue = useUtilityStore((state) => state.chatValueStore);
|
||||
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
|
||||
|
|
@ -185,28 +234,28 @@ export default function IOModal({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sessions = new Set<string>();
|
||||
messages
|
||||
.filter((message) => message.flow_id === currentFlowId)
|
||||
.forEach((row) => {
|
||||
sessions.add(row.session_id);
|
||||
});
|
||||
setSessions((prev) => {
|
||||
if (prev.length < Array.from(sessions).length) {
|
||||
// set the new session as visible
|
||||
setvisibleSession(
|
||||
Array.from(sessions)[Array.from(sessions).length - 1],
|
||||
);
|
||||
}
|
||||
return Array.from(sessions);
|
||||
});
|
||||
if (newChatOnPlayground && !sessionsLoading) {
|
||||
const handleRefetchAndSetSession = async () => {
|
||||
try {
|
||||
const result = await refetchSessions();
|
||||
if (result.data?.sessions && result.data.sessions.length > 0) {
|
||||
setvisibleSession(
|
||||
result.data.sessions[result.data.sessions.length - 1],
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error refetching sessions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
handleRefetchAndSetSession();
|
||||
setNewChatOnPlayground(false);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleSession) {
|
||||
setSessionId(
|
||||
`Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`,
|
||||
);
|
||||
setSessionId(createNewSessionName());
|
||||
setCurrentSessionId(currentFlowId);
|
||||
} else if (visibleSession) {
|
||||
setSessionId(visibleSession);
|
||||
|
|
@ -271,6 +320,35 @@ export default function IOModal({
|
|||
: getNumberFromString(flowGradient ?? flowId ?? "")) %
|
||||
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 (
|
||||
<BaseModal
|
||||
open={open}
|
||||
|
|
@ -336,7 +414,7 @@ export default function IOModal({
|
|||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
{sidebarOpen && !sessionsLoading && (
|
||||
<SidebarOpenView
|
||||
sessions={sessions}
|
||||
setSelectedViewField={setSelectedViewField}
|
||||
|
|
@ -345,6 +423,7 @@ export default function IOModal({
|
|||
visibleSession={visibleSession}
|
||||
selectedViewField={selectedViewField}
|
||||
playgroundPage={!!playgroundPage}
|
||||
setActiveSession={setActiveSession}
|
||||
/>
|
||||
)}
|
||||
{sidebarOpen && showPublishOptions && (
|
||||
|
|
@ -383,7 +462,7 @@ export default function IOModal({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex h-full min-w-96 flex-grow bg-background">
|
||||
{selectedViewField && (
|
||||
{selectedViewField && !sessionsLoading && (
|
||||
<SelectedViewField
|
||||
selectedViewField={selectedViewField}
|
||||
setSelectedViewField={setSelectedViewField}
|
||||
|
|
@ -8,4 +8,5 @@ export type SidebarOpenViewProps = {
|
|||
visibleSession: string | undefined;
|
||||
selectedViewField: { type: string; id: string } | undefined;
|
||||
playgroundPage: boolean;
|
||||
setActiveSession: (session: string) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1061,6 +1061,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
);
|
||||
set({ dismissedNodes: newDismissedNodes });
|
||||
},
|
||||
setNewChatOnPlayground: (newChat: boolean) => {
|
||||
set({ newChatOnPlayground: newChat });
|
||||
},
|
||||
newChatOnPlayground: false,
|
||||
}));
|
||||
|
||||
export default useFlowStore;
|
||||
|
|
|
|||
|
|
@ -285,4 +285,6 @@ export type FlowStoreType = {
|
|||
setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void;
|
||||
clearEdgesRunningByNodes: () => Promise<void>;
|
||||
updateToolMode: (nodeId: string, toolMode: boolean) => void;
|
||||
newChatOnPlayground: boolean;
|
||||
setNewChatOnPlayground: (newChat: boolean) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -902,3 +902,93 @@ export function getOS() {
|
|||
|
||||
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