feat: AI flow builder, visibility model, dashboard tabs, fork UI (#88)

- AI flow builder: scaffold → branch detail → assemble → review flow
- Generate All one-click branch generation with stop/cancel
- Regenerate scaffold suggestions button
- 3-action review screen: Start Flow, Open in Editor, Build Another
- Fix Publish button gated behind !isDirty
- Fix visibility column enforcement in tree access filter
- Add ?visibility filter and author_name to GET /trees
- Dashboard tabbed flows: My Flows / My Team / Public / All
- Create button in My Flows tab, window focus reload (stale data fix)
- Fork UI with optional reason modal
- Fix account_id nullability in User type and schema
- Keep is_public and visibility in sync on updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #88.
This commit is contained in:
chihlasm
2026-02-24 07:40:44 -05:00
committed by GitHub
parent 97cd297f46
commit ed4ab059bf
41 changed files with 1909 additions and 315 deletions

View File

@@ -97,7 +97,16 @@ CORRECTIVE_PROMPT_TEMPLATE = """Your previous JSON was invalid for ResolutionFlo
Validation errors:
{error_list}
IMPORTANT: If any error mentions a next_node_id referencing a non-existent child, you must ensure every option's next_node_id exactly matches the "id" field of one of the node's direct children. The child node must exist in the "children" array of the same parent node.
CRITICAL RULES TO FIX THESE ERRORS:
1. Decision node options → next_node_id MUST match the "id" of a direct child in that SAME decision node's "children" array.
Example: if decision node has children [A, B, C], then option next_node_id must be "A", "B", or "C".
2. Action node → next_node_id MUST match the "id" of a SIBLING node — another child of the SAME parent decision node.
Example: if parent decision has children [action-1, solution-1, solution-2], then action-1's next_node_id must be "solution-1" or "solution-2".
The next node must ALREADY EXIST in the parent's children array — do NOT nest the next node inside the action node.
3. Every referenced ID must physically exist somewhere in the tree as a node with that exact "id" value.
Return a corrected full JSON object only. No markdown, no prose, no code fences.
Fix ALL listed errors while maintaining the same troubleshooting/procedural logic."""
@@ -224,6 +233,12 @@ async def generate_branch_detail(
len(response.content),
response.usage.output_tokens,
)
if response.stop_reason == "max_tokens":
logger.warning(
"branch_detail attempt=%d hit max_tokens limit (%d output tokens) — response may be truncated",
attempt,
response.usage.output_tokens,
)
raw_text = _strip_markdown_fences(response.content[0].text) if response.content else ""
if not raw_text:
logger.warning("branch_detail attempt=%d returned empty text, stop_reason=%s", attempt, response.stop_reason)

View File

@@ -40,7 +40,8 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
# Collect all node IDs and validate structure
all_ids: set[str] = set()
all_referenced_ids: set[str] = set()
all_referenced_ids: set[str] = set() # option next_node_ids (already checked locally)
action_next_ids: set[str] = set() # action next_node_ids (checked globally below)
node_count = 0
solution_count = 0
@@ -121,6 +122,7 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
)
else:
all_referenced_ids.add(next_id)
action_next_ids.add(next_id)
elif node_type == "solution":
solution_count += 1
@@ -131,6 +133,13 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
_validate_node(tree, "root")
# Check that all action next_node_ids actually exist in the tree
for ref_id in action_next_ids:
if ref_id not in all_ids:
errors.append(
f"Action next_node_id '{ref_id}' references a node that does not exist in the tree"
)
# Global checks
if node_count < 5:
errors.append(

View File

@@ -16,24 +16,32 @@ if TYPE_CHECKING:
def build_tree_access_filter(current_user: User):
"""Build the access filter for trees based on user permissions.
Returns trees that are:
- All trees (for super admins)
- Default/system trees (visible to all)
- Public trees
- User's own trees
- Trees from user's account
Visibility rules:
- super_admin: sees everything
- is_default: visible to all authenticated users
- visibility='public': visible to all authenticated users
- author_id == me: always visible (regardless of visibility setting)
- visibility='team' AND account_id == mine: visible to account members
- visibility='private': only visible to author (covered by author_id check above)
- visibility='link': only visible to author (share token access is handled separately)
"""
from app.models.tree import Tree
if current_user.is_super_admin:
return sa_true()
conditions = [
Tree.is_default == True,
Tree.is_public == True,
Tree.visibility == 'public',
Tree.author_id == current_user.id,
]
if current_user.account_id:
conditions.append(Tree.account_id == current_user.account_id)
conditions.append(
and_(
Tree.visibility == 'team',
Tree.account_id == current_user.account_id
)
)
return or_(*conditions)