mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 596fcec56c |
@@ -84,10 +84,6 @@ jobs:
|
||||
run: |
|
||||
python -m pip index versions openhands-ai > openhands_versions.txt
|
||||
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
|
||||
# Ensure requirements.txt ends with newline before appending
|
||||
if [ -f requirements.txt ] && [ -s requirements.txt ]; then
|
||||
sed -i -e '$a\' requirements.txt
|
||||
fi
|
||||
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
|
||||
cat requirements.txt
|
||||
|
||||
|
||||
@@ -176,7 +176,6 @@ evaluation/gorilla/data
|
||||
evaluation/toolqa/data
|
||||
evaluation/scienceagentbench/benchmark
|
||||
evaluation/commit0_bench/repos
|
||||
evaluation/visualcodebench/
|
||||
|
||||
# openhands resolver
|
||||
output/
|
||||
|
||||
@@ -1,674 +0,0 @@
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from difflib import SequenceMatcher
|
||||
from io import BytesIO
|
||||
|
||||
from bs4 import BeautifulSoup, Comment, NavigableString, Tag
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from colormath.color_conversions import convert_color
|
||||
from colormath.color_diff import delta_e_cie2000
|
||||
from colormath.color_objects import LabColor, sRGBColor
|
||||
from PIL import Image, ImageChops, ImageColor
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
from transformers import CLIPModel, CLIPProcessor
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def calculate_similarity(block1, block2):
|
||||
"""Calculate text similarity between two blocks using SequenceMatcher."""
|
||||
text_similarity = SequenceMatcher(None, block1['text'], block2['text']).ratio()
|
||||
return text_similarity
|
||||
|
||||
|
||||
def adjust_cost_for_context(cost_matrix, consecutive_bonus=1.0, window_size=20):
|
||||
"""Adjust cost matrix by considering context similarity."""
|
||||
if window_size <= 0:
|
||||
return cost_matrix
|
||||
|
||||
n, m = cost_matrix.shape
|
||||
adjusted_cost_matrix = np.copy(cost_matrix)
|
||||
|
||||
for i in range(n):
|
||||
for j in range(m):
|
||||
if adjusted_cost_matrix[i][j] >= -0.5:
|
||||
continue
|
||||
nearby_matrix = cost_matrix[
|
||||
max(0, i - window_size) : min(n, i + window_size + 1),
|
||||
max(0, j - window_size) : min(m, j + window_size + 1),
|
||||
]
|
||||
flattened_array = nearby_matrix.flatten()
|
||||
sorted_array = np.sort(flattened_array)[::-1]
|
||||
sorted_array = np.delete(
|
||||
sorted_array, np.where(sorted_array == cost_matrix[i, j])[0][0]
|
||||
)
|
||||
top_k_elements = sorted_array[-window_size * 2 :]
|
||||
bonus = consecutive_bonus * np.sum(top_k_elements)
|
||||
adjusted_cost_matrix[i][j] += bonus
|
||||
return adjusted_cost_matrix
|
||||
|
||||
|
||||
def create_cost_matrix(A, B):
|
||||
"""Create cost matrix for block matching."""
|
||||
n = len(A)
|
||||
m = len(B)
|
||||
cost_matrix = np.zeros((n, m))
|
||||
for i in range(n):
|
||||
for j in range(m):
|
||||
cost_matrix[i, j] = -calculate_similarity(A[i], B[j])
|
||||
return cost_matrix
|
||||
|
||||
|
||||
def calculate_distance_max_1d(x1, y1, x2, y2):
|
||||
"""Calculate maximum 1D distance between points."""
|
||||
return max(abs(x2 - x1), abs(y2 - y1))
|
||||
|
||||
|
||||
def calculate_ratio(h1, h2):
|
||||
"""Calculate ratio between two heights."""
|
||||
return max(h1, h2) / min(h1, h2)
|
||||
|
||||
|
||||
def rgb_to_lab(rgb):
|
||||
"""Convert RGB color to Lab color space."""
|
||||
rgb_color = sRGBColor(rgb[0], rgb[1], rgb[2], is_upscaled=True)
|
||||
lab_color = convert_color(rgb_color, LabColor)
|
||||
return lab_color
|
||||
|
||||
|
||||
def color_similarity_ciede2000(rgb1, rgb2):
|
||||
"""Calculate color similarity using CIEDE2000 formula."""
|
||||
lab1 = rgb_to_lab(rgb1)
|
||||
lab2 = rgb_to_lab(rgb2)
|
||||
delta_e = delta_e_cie2000(lab1, lab2)
|
||||
similarity = max(0, 1 - (delta_e / 100))
|
||||
return similarity
|
||||
|
||||
|
||||
def merge_blocks_wo_check(block1, block2):
|
||||
"""Merge two blocks without additional checks."""
|
||||
merged_text = block1['text'] + ' ' + block2['text']
|
||||
x_min = min(block1['bbox'][0], block2['bbox'][0])
|
||||
y_min = min(block1['bbox'][1], block2['bbox'][1])
|
||||
x_max = max(
|
||||
block1['bbox'][0] + block1['bbox'][2], block2['bbox'][0] + block2['bbox'][2]
|
||||
)
|
||||
y_max = max(
|
||||
block1['bbox'][1] + block1['bbox'][3], block2['bbox'][1] + block2['bbox'][3]
|
||||
)
|
||||
merged_bbox = (x_min, y_min, x_max - x_min, y_max - y_min)
|
||||
merged_color = tuple(
|
||||
(color1 + color2) // 2
|
||||
for color1, color2 in zip(block1['color'], block2['color'])
|
||||
)
|
||||
return {'text': merged_text, 'bbox': merged_bbox, 'color': merged_color}
|
||||
|
||||
|
||||
def find_maximum_matching(A, B, consecutive_bonus, window_size):
|
||||
"""Find maximum matching between two sets of blocks."""
|
||||
cost_matrix = create_cost_matrix(A, B)
|
||||
cost_matrix = adjust_cost_for_context(cost_matrix, consecutive_bonus, window_size)
|
||||
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
||||
current_cost = cost_matrix[row_ind, col_ind].tolist()
|
||||
return list(zip(row_ind, col_ind)), current_cost, cost_matrix
|
||||
|
||||
|
||||
def remove_indices(lst, indices):
|
||||
"""Remove indices from list in reverse order."""
|
||||
for index in sorted(indices, reverse=True):
|
||||
if index < len(lst):
|
||||
lst.pop(index)
|
||||
return lst
|
||||
|
||||
|
||||
def merge_blocks_by_list(blocks, merge_list):
|
||||
"""Merge blocks according to merge list."""
|
||||
pop_list = []
|
||||
while merge_list:
|
||||
i = merge_list[0][0]
|
||||
j = merge_list[0][1]
|
||||
blocks[i] = merge_blocks_wo_check(blocks[i], blocks[j])
|
||||
pop_list.append(j)
|
||||
merge_list.pop(0)
|
||||
if merge_list:
|
||||
new_merge_list = []
|
||||
for k in range(len(merge_list)):
|
||||
if (
|
||||
merge_list[k][0] != i
|
||||
and merge_list[k][1] != i
|
||||
and merge_list[k][0] != j
|
||||
and merge_list[k][1] != j
|
||||
):
|
||||
new_merge_list.append(merge_list[k])
|
||||
merge_list = new_merge_list
|
||||
remove_indices(blocks, pop_list)
|
||||
return blocks
|
||||
|
||||
|
||||
def difference_of_means(list1, list2):
|
||||
"""Calculate difference of means between two lists."""
|
||||
counter1 = Counter(list1)
|
||||
counter2 = Counter(list2)
|
||||
|
||||
for element in set(list1) & set(list2):
|
||||
common_count = min(counter1[element], counter2[element])
|
||||
counter1[element] -= common_count
|
||||
counter2[element] -= common_count
|
||||
|
||||
unique_list1 = [item for item in counter1.elements()]
|
||||
unique_list2 = [item for item in counter2.elements()]
|
||||
|
||||
mean_list1 = sum(unique_list1) / len(unique_list1) if unique_list1 else 0
|
||||
mean_list2 = sum(unique_list2) / len(unique_list2) if unique_list2 else 0
|
||||
|
||||
if mean_list1 - mean_list2 > 0:
|
||||
if min(unique_list1) > min(unique_list2):
|
||||
return mean_list1 - mean_list2
|
||||
return 0.0
|
||||
return mean_list1 - mean_list2
|
||||
|
||||
|
||||
def find_possible_merge(A, B, consecutive_bonus, window_size, debug=False):
|
||||
"""Find possible merges between blocks."""
|
||||
merge_bonus = 0.0
|
||||
merge_windows = 1
|
||||
|
||||
def sortFn(value):
|
||||
return value[2]
|
||||
|
||||
while True:
|
||||
A_changed = False
|
||||
B_changed = False
|
||||
|
||||
matching, current_cost, cost_matrix = find_maximum_matching(
|
||||
A, B, merge_bonus, merge_windows
|
||||
)
|
||||
|
||||
if len(A) >= 2:
|
||||
merge_list = []
|
||||
for i in range(len(A) - 1):
|
||||
new_A = deepcopy(A)
|
||||
new_A[i] = merge_blocks_wo_check(new_A[i], new_A[i + 1])
|
||||
new_A.pop(i + 1)
|
||||
updated_matching, updated_cost, _ = find_maximum_matching(
|
||||
new_A, B, merge_bonus, merge_windows
|
||||
)
|
||||
diff = difference_of_means(current_cost, updated_cost)
|
||||
if diff > 0.05:
|
||||
merge_list.append([i, i + 1, diff])
|
||||
|
||||
merge_list.sort(key=sortFn, reverse=True)
|
||||
if merge_list:
|
||||
A_changed = True
|
||||
A = merge_blocks_by_list(A, merge_list)
|
||||
matching, current_cost, cost_matrix = find_maximum_matching(
|
||||
A, B, merge_bonus, merge_windows
|
||||
)
|
||||
|
||||
if len(B) >= 2:
|
||||
merge_list = []
|
||||
for i in range(len(B) - 1):
|
||||
new_B = deepcopy(B)
|
||||
new_B[i] = merge_blocks_wo_check(new_B[i], new_B[i + 1])
|
||||
new_B.pop(i + 1)
|
||||
updated_matching, updated_cost, _ = find_maximum_matching(
|
||||
A, new_B, merge_bonus, merge_windows
|
||||
)
|
||||
diff = difference_of_means(current_cost, updated_cost)
|
||||
if diff > 0.05:
|
||||
merge_list.append([i, i + 1, diff])
|
||||
|
||||
merge_list.sort(key=sortFn, reverse=True)
|
||||
if merge_list:
|
||||
B_changed = True
|
||||
B = merge_blocks_by_list(B, merge_list)
|
||||
matching, current_cost, cost_matrix = find_maximum_matching(
|
||||
A, B, merge_bonus, merge_windows
|
||||
)
|
||||
|
||||
if not A_changed and not B_changed:
|
||||
break
|
||||
|
||||
matching, _, _ = find_maximum_matching(A, B, consecutive_bonus, window_size)
|
||||
return A, B, matching
|
||||
|
||||
|
||||
def merge_blocks_by_bbox(blocks):
|
||||
"""Merge blocks with same bounding box."""
|
||||
merged_blocks = {}
|
||||
for block in blocks:
|
||||
bbox = tuple(block['bbox'])
|
||||
if bbox in merged_blocks:
|
||||
existing_block = merged_blocks[bbox]
|
||||
existing_block['text'] += ' ' + block['text']
|
||||
existing_block['color'] = [
|
||||
(ec + c) / 2 for ec, c in zip(existing_block['color'], block['color'])
|
||||
]
|
||||
else:
|
||||
merged_blocks[bbox] = block
|
||||
return list(merged_blocks.values())
|
||||
|
||||
|
||||
def mask_bounding_boxes_with_inpainting(image, bounding_boxes):
|
||||
"""Mask bounding boxes in image using inpainting."""
|
||||
image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
mask = np.zeros(image_cv.shape[:2], dtype=np.uint8)
|
||||
height, width = image_cv.shape[:2]
|
||||
|
||||
for bbox in bounding_boxes:
|
||||
x_ratio, y_ratio, w_ratio, h_ratio = bbox
|
||||
x = int(x_ratio * width)
|
||||
y = int(y_ratio * height)
|
||||
w = int(w_ratio * width)
|
||||
h = int(h_ratio * height)
|
||||
mask[y : y + h, x : x + w] = 255
|
||||
|
||||
inpainted_image = cv2.inpaint(image_cv, mask, 3, cv2.INPAINT_TELEA)
|
||||
return Image.fromarray(cv2.cvtColor(inpainted_image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
|
||||
def rescale_and_mask(image, blocks):
|
||||
"""Rescale image and mask blocks."""
|
||||
if blocks:
|
||||
image = mask_bounding_boxes_with_inpainting(image, blocks)
|
||||
|
||||
width, height = image.size
|
||||
if width < height:
|
||||
new_size = (width, width)
|
||||
else:
|
||||
new_size = (height, height)
|
||||
|
||||
return image.resize(new_size, Image.LANCZOS)
|
||||
|
||||
|
||||
def calculate_clip_similarity(image1, image2, blocks1, blocks2):
|
||||
"""Calculate CLIP similarity between two images."""
|
||||
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
|
||||
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
model = model.to(device)
|
||||
|
||||
# Mask and preprocess images
|
||||
image1_masked = rescale_and_mask(image1, [block['bbox'] for block in blocks1])
|
||||
image2_masked = rescale_and_mask(image2, [block['bbox'] for block in blocks2])
|
||||
inputs = processor(
|
||||
images=[image1_masked, image2_masked], return_tensors='pt', padding=True
|
||||
)
|
||||
inputs = {k: v.to(device) for k, v in inputs.items()}
|
||||
|
||||
# Calculate features and similarity
|
||||
with torch.no_grad():
|
||||
image_features = model.get_image_features(**inputs)
|
||||
image_features1 = image_features[0].unsqueeze(0)
|
||||
image_features2 = image_features[1].unsqueeze(0)
|
||||
image_features1 /= image_features1.norm(dim=-1, keepdim=True)
|
||||
image_features2 /= image_features2.norm(dim=-1, keepdim=True)
|
||||
similarity = (image_features1 @ image_features2.T).item()
|
||||
|
||||
return similarity
|
||||
|
||||
|
||||
def rgb_to_hex(rgb):
|
||||
"""Convert an RGB tuple to hexadecimal format."""
|
||||
return '{:02X}{:02X}{:02X}'.format(*rgb)
|
||||
|
||||
|
||||
class ColorPool:
|
||||
def __init__(self, offset=0):
|
||||
color_values = list(range(10, 251, 16))
|
||||
color_list = [((r + offset) % 256, (g + offset) % 256, (b + offset) % 256)
|
||||
for r in color_values for g in color_values for b in color_values]
|
||||
self.color_pool = [rgb_to_hex(color) for color in color_list]
|
||||
|
||||
def pop_color(self):
|
||||
if self.color_pool:
|
||||
return self.color_pool.pop()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def process_html_str(html_str, offset=0):
|
||||
"""Process HTML string to assign unique colors to text elements."""
|
||||
soup = BeautifulSoup(html_str, 'html.parser')
|
||||
|
||||
def update_style(element, property_name, value):
|
||||
important_value = f"{value} !important"
|
||||
styles = element.attrs.get('style', '').split(';')
|
||||
updated_styles = [s for s in styles if not s.strip().startswith(property_name) and len(s.strip()) > 0]
|
||||
updated_styles.append(f"{property_name}: {important_value}")
|
||||
element['style'] = '; '.join(updated_styles).strip()
|
||||
|
||||
# Set background color of all elements to transparent white
|
||||
for element in soup.find_all(True):
|
||||
update_style(element, 'background-color', 'rgba(255, 255, 255, 0.0)')
|
||||
|
||||
color_pool = ColorPool(offset)
|
||||
text_tags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'a', 'b', 'li',
|
||||
'table', 'td', 'th', 'button', 'footer', 'header', 'figcaption']
|
||||
|
||||
for tag in soup.find_all(text_tags):
|
||||
color = f"#{color_pool.pop_color()}"
|
||||
update_style(tag, 'color', color)
|
||||
update_style(tag, 'opacity', '1.0')
|
||||
|
||||
return str(soup)
|
||||
|
||||
|
||||
def similar(n1, n2):
|
||||
"""Check if two numbers are similar within a threshold."""
|
||||
return abs(n1 - n2) <= 8
|
||||
|
||||
|
||||
def find_different_pixels(image1, image2):
|
||||
"""Find pixels that differ between two images."""
|
||||
if image1.size != image2.size:
|
||||
logger.warning("Images are not the same size")
|
||||
return None
|
||||
|
||||
image1 = image1.convert('RGB')
|
||||
image2 = image2.convert('RGB')
|
||||
pixels1 = image1.load()
|
||||
pixels2 = image2.load()
|
||||
different_pixels = []
|
||||
|
||||
for x in range(image1.size[0]):
|
||||
for y in range(image1.size[1]):
|
||||
r1, g1, b1 = pixels1[x, y]
|
||||
r2, g2, b2 = pixels2[x, y]
|
||||
if similar((r1 + 50) % 256, r2) and similar((g1 + 50) % 256, g2) and similar((b1 + 50) % 256, b2):
|
||||
different_pixels.append((y, x))
|
||||
|
||||
return np.stack(different_pixels) if different_pixels else None
|
||||
|
||||
|
||||
def extract_text_with_color(html_str):
|
||||
"""Extract text and color information from HTML string."""
|
||||
def get_color(tag):
|
||||
if 'style' in tag.attrs:
|
||||
styles = tag['style'].split(';')
|
||||
color_style = [s for s in styles if 'color' in s and 'background-color' not in s]
|
||||
if color_style:
|
||||
color = color_style[-1].split(':')[1].strip().replace(" !important", "")
|
||||
if color[0] == "#":
|
||||
return color
|
||||
else:
|
||||
try:
|
||||
if color.startswith('rgb'):
|
||||
color = tuple(map(int, color[4:-1].split(',')))
|
||||
else:
|
||||
color = ImageColor.getrgb(color)
|
||||
return '#{:02x}{:02x}{:02x}'.format(*color)
|
||||
except ValueError:
|
||||
logger.warning(f"Unable to identify or convert color: {color}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def extract_text_recursive(element, parent_color='#000000'):
|
||||
if isinstance(element, Comment):
|
||||
return None
|
||||
elif isinstance(element, NavigableString):
|
||||
text = element.strip()
|
||||
return (text, parent_color) if text else None
|
||||
elif isinstance(element, Tag):
|
||||
current_color = get_color(element) or parent_color
|
||||
children_texts = filter(None, [extract_text_recursive(child, current_color)
|
||||
for child in element.children])
|
||||
return list(children_texts)
|
||||
|
||||
soup = BeautifulSoup(html_str, 'html.parser')
|
||||
body = soup.body
|
||||
return extract_text_recursive(body) if body else []
|
||||
|
||||
|
||||
def flatten_tree(tree):
|
||||
"""Flatten a nested tree structure into a list."""
|
||||
flat_list = []
|
||||
def flatten(node):
|
||||
if isinstance(node, list):
|
||||
for item in node:
|
||||
flatten(item)
|
||||
else:
|
||||
flat_list.append(node)
|
||||
flatten(tree)
|
||||
return flat_list
|
||||
|
||||
|
||||
def get_blocks_from_image_diff_pixels(image, html_text_color_tree, different_pixels):
|
||||
"""Extract text blocks from image using color differences."""
|
||||
image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
x_w = image_cv.shape[0]
|
||||
y_w = image_cv.shape[1]
|
||||
|
||||
def hex_to_bgr(hex_color):
|
||||
hex_color = hex_color.lstrip('#')
|
||||
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
return rgb[::-1]
|
||||
|
||||
def get_intersect(arr1, arr2):
|
||||
arr1_reshaped = arr1.view([('', arr1.dtype)] * arr1.shape[1])
|
||||
arr2_reshaped = arr2.view([('', arr2.dtype)] * arr2.shape[1])
|
||||
common_rows = np.intersect1d(arr1_reshaped, arr2_reshaped)
|
||||
return common_rows.view(arr1.dtype).reshape(-1, arr1.shape[1])
|
||||
|
||||
blocks = []
|
||||
for item in html_text_color_tree:
|
||||
try:
|
||||
color = np.array(hex_to_bgr(item[1]), dtype="uint8")
|
||||
except:
|
||||
continue
|
||||
|
||||
lower = color - 4
|
||||
upper = color + 4
|
||||
mask = cv2.inRange(image_cv, lower, upper)
|
||||
coords = np.column_stack(np.where(mask > 0))
|
||||
coords = get_intersect(coords, different_pixels)
|
||||
|
||||
if coords.size == 0:
|
||||
continue
|
||||
|
||||
x_min, y_min = np.min(coords, axis=0)
|
||||
x_max, y_max = np.max(coords, axis=0)
|
||||
|
||||
# Get average color from original image
|
||||
color_coords = coords.copy()
|
||||
color_coords = color_coords[color_coords[:, 0] <= x_max]
|
||||
color_coords = color_coords[color_coords[:, 1] <= y_max]
|
||||
colors = [image_cv[x, y] for x, y in color_coords]
|
||||
avg_color = tuple(map(int, np.mean(colors, axis=0)))[::-1] # Convert BGR to RGB
|
||||
|
||||
blocks.append({
|
||||
'text': item[0].lower(),
|
||||
'bbox': (y_min / y_w, x_min / x_w, (y_max - y_min + 1) / y_w, (x_max - x_min + 1) / x_w),
|
||||
'color': avg_color
|
||||
})
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def get_blocks_from_html(html_str, image1):
|
||||
"""Extract text blocks from HTML and image."""
|
||||
# Process HTML with two different color offsets
|
||||
html_str_1 = process_html_str(html_str, offset=0)
|
||||
html_str_2 = process_html_str(html_str, offset=50)
|
||||
|
||||
# Render both HTML versions to images
|
||||
# TODO: Screenshot html_str_2
|
||||
filter_color = (255, 0, 0)
|
||||
image2 = Image.new("RGB", image1.size, filter_color)
|
||||
|
||||
|
||||
# Find pixels that differ between the two rendered images
|
||||
different_pixels = find_different_pixels(image1, image2)
|
||||
if different_pixels is None:
|
||||
logger.warning("Unable to get pixels with different colors")
|
||||
return []
|
||||
|
||||
# Extract text and color information from HTML
|
||||
html_text_color_tree = flatten_tree(extract_text_with_color(html_str_1))
|
||||
try:
|
||||
blocks = get_blocks_from_image_diff_pixels(image1, html_text_color_tree, different_pixels)
|
||||
except Exception as e:
|
||||
logger.warning(f"Unable to get blocks: {e}")
|
||||
return []
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def evaluate(task, generated_img):
|
||||
"""Evaluate generated image against reference image using multiple metrics."""
|
||||
# Load reference image
|
||||
post_image = task['post_image']
|
||||
|
||||
# Extract blocks from HTML and images
|
||||
post_blocks = get_blocks_from_html(task['post_html'], post_image)
|
||||
gen_blocks = get_blocks_from_html(task['gen_html'], generated_img)
|
||||
|
||||
print("block details", post_blocks, gen_blocks)
|
||||
if not post_blocks or not gen_blocks:
|
||||
# Fallback to basic CLIP and pixel comparison if no blocks available
|
||||
clip_score = calculate_clip_similarity(post_image, generated_img, [], [])
|
||||
logger.info(f'CLIP similarity score: {clip_score}')
|
||||
|
||||
# Pixel comparison
|
||||
diff = ImageChops.difference(generated_img, post_image)
|
||||
pixel_match = not diff.getbbox()
|
||||
logger.info(
|
||||
f"Pixel difference analysis: {'No difference' if pixel_match else 'Differences found'}"
|
||||
)
|
||||
|
||||
return clip_score > 0.95 or pixel_match
|
||||
|
||||
# Merge blocks with same bounding boxes
|
||||
post_blocks = merge_blocks_by_bbox(post_blocks)
|
||||
gen_blocks = merge_blocks_by_bbox(gen_blocks)
|
||||
|
||||
# Find optimal block matching
|
||||
consecutive_bonus, window_size = 0.1, 1
|
||||
gen_blocks_m, post_blocks_m, matching = find_possible_merge(
|
||||
gen_blocks, deepcopy(post_blocks), consecutive_bonus, window_size
|
||||
)
|
||||
|
||||
# Filter matches with low similarity
|
||||
filtered_matching = []
|
||||
for i, j in matching:
|
||||
text_similarity = calculate_similarity(gen_blocks_m[i], post_blocks_m[j])
|
||||
if text_similarity >= 0.5:
|
||||
filtered_matching.append([i, j, text_similarity])
|
||||
matching = filtered_matching
|
||||
|
||||
if not matching:
|
||||
logger.warning('No matching blocks found')
|
||||
clip_score = calculate_clip_similarity(
|
||||
post_image, generated_img, gen_blocks, post_blocks
|
||||
)
|
||||
return clip_score > 0.95
|
||||
|
||||
# Calculate metrics for matched blocks
|
||||
indices1 = [item[0] for item in matching]
|
||||
indices2 = [item[1] for item in matching]
|
||||
|
||||
# Calculate unmatched areas
|
||||
unmatched_area_1 = sum(
|
||||
block['bbox'][2] * block['bbox'][3]
|
||||
for i, block in enumerate(gen_blocks_m)
|
||||
if i not in indices1
|
||||
)
|
||||
unmatched_area_2 = sum(
|
||||
block['bbox'][2] * block['bbox'][3]
|
||||
for j, block in enumerate(post_blocks_m)
|
||||
if j not in indices2
|
||||
)
|
||||
total_unmatched_area = unmatched_area_1 + unmatched_area_2
|
||||
|
||||
# Calculate metrics for matched blocks
|
||||
matched_areas = []
|
||||
text_scores = []
|
||||
position_scores = []
|
||||
color_scores = []
|
||||
|
||||
for i, j, text_similarity in matching:
|
||||
# Area
|
||||
block_area = (
|
||||
gen_blocks_m[i]['bbox'][2] * gen_blocks_m[i]['bbox'][3]
|
||||
+ post_blocks_m[j]['bbox'][2] * post_blocks_m[j]['bbox'][3]
|
||||
)
|
||||
matched_areas.append(block_area)
|
||||
|
||||
# Position similarity
|
||||
position_similarity = 1 - calculate_distance_max_1d(
|
||||
gen_blocks_m[i]['bbox'][0] + gen_blocks_m[i]['bbox'][2] / 2,
|
||||
gen_blocks_m[i]['bbox'][1] + gen_blocks_m[i]['bbox'][3] / 2,
|
||||
post_blocks_m[j]['bbox'][0] + post_blocks_m[j]['bbox'][2] / 2,
|
||||
post_blocks_m[j]['bbox'][1] + post_blocks_m[j]['bbox'][3] / 2,
|
||||
)
|
||||
|
||||
# Color similarity
|
||||
color_similarity = color_similarity_ciede2000(
|
||||
gen_blocks_m[i]['color'], post_blocks_m[j]['color']
|
||||
)
|
||||
|
||||
text_scores.append(text_similarity)
|
||||
position_scores.append(position_similarity)
|
||||
color_scores.append(color_similarity)
|
||||
|
||||
# Calculate final scores
|
||||
total_area = sum(matched_areas) + total_unmatched_area
|
||||
size_score = sum(matched_areas) / total_area if total_area > 0 else 0
|
||||
text_score = np.mean(text_scores) if text_scores else 0
|
||||
position_score = np.mean(position_scores) if position_scores else 0
|
||||
color_score = np.mean(color_scores) if color_scores else 0
|
||||
clip_score = calculate_clip_similarity(
|
||||
post_image, generated_img, gen_blocks, post_blocks
|
||||
)
|
||||
|
||||
# Combine scores with equal weights
|
||||
final_score = 0.2 * (
|
||||
size_score + text_score + position_score + color_score + clip_score
|
||||
)
|
||||
|
||||
logger.info('Evaluation scores:')
|
||||
logger.info(f'- Size score: {size_score:.3f}')
|
||||
logger.info(f'- Text score: {text_score:.3f}')
|
||||
logger.info(f'- Position score: {position_score:.3f}')
|
||||
logger.info(f'- Color score: {color_score:.3f}')
|
||||
logger.info(f'- CLIP score: {clip_score:.3f}')
|
||||
logger.info(f'- Final score: {final_score:.3f}')
|
||||
|
||||
return final_score > 0.8 # Consider it a match if final score > 80%
|
||||
|
||||
|
||||
def png_to_bytes(png):
|
||||
buffer = BytesIO()
|
||||
png.save(buffer, format='PNG')
|
||||
image_bytes = buffer.getvalue()
|
||||
return image_bytes
|
||||
|
||||
|
||||
def bytes_to_image(image_bytes):
|
||||
"""Convert bytes to a Pillow Image object."""
|
||||
return Image.open(BytesIO(image_bytes))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
first_image = Image.open('./evaluation/visualcodebench/data/1/post.png')
|
||||
image = Image.open('./evaluation/visualcodebench/data/1/prev.png')
|
||||
|
||||
|
||||
html_file = open('./evaluation/visualcodebench/data/1/post/index.html', 'r')
|
||||
first_html = html_file.read()
|
||||
html_file.close()
|
||||
|
||||
html_file = open('./evaluation/visualcodebench/data/1/prev/index.html', 'r')
|
||||
gen_html = html_file.read()
|
||||
html_file.close()
|
||||
|
||||
|
||||
|
||||
sample = {'post_image': first_image, "post_html": first_html, "gen_html": gen_html}
|
||||
|
||||
|
||||
|
||||
evaluate(sample, image)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import base64
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import pandas as pd
|
||||
from huggingface_hub import snapshot_download
|
||||
from PIL import PngImagePlugin
|
||||
from tqdm import tqdm
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
REPO_DOWNLOAD_DIR = (
|
||||
'./evaluation/visualcodebench/' # Directory to store the downloaded repository
|
||||
)
|
||||
|
||||
|
||||
def download_repository():
|
||||
"""
|
||||
Download the entire repository from Hugging Face Hub.
|
||||
This function clones the repository into REPO_DOWNLOAD_DIR.
|
||||
"""
|
||||
repo_id = 'rvmalhot/VisualCodeBench'
|
||||
try:
|
||||
logger.info(f"Downloading repository '{repo_id}'...")
|
||||
snapshot_download(
|
||||
repo_id=repo_id,
|
||||
local_dir=REPO_DOWNLOAD_DIR,
|
||||
repo_type='dataset',
|
||||
ignore_patterns=None, # Download all files
|
||||
)
|
||||
logger.info(f"Repository downloaded to '{REPO_DOWNLOAD_DIR}'.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading repository '{repo_id}': {e}")
|
||||
raise e
|
||||
|
||||
|
||||
def format_task_dict(example):
|
||||
instance_id = example['id']
|
||||
prev_remote_path = os.path.join(REPO_DOWNLOAD_DIR, f'data/{instance_id}/prev')
|
||||
post_remote_path = os.path.join(REPO_DOWNLOAD_DIR, f'data/{instance_id}/post')
|
||||
|
||||
# Check if 'prev' and 'post' directories exist
|
||||
prev_exists = os.path.exists(prev_remote_path)
|
||||
post_exists = os.path.exists(post_remote_path)
|
||||
|
||||
if prev_exists and post_exists:
|
||||
skip = False
|
||||
else:
|
||||
skip = True
|
||||
|
||||
task = {
|
||||
'instance_id': instance_id,
|
||||
'prev_image': example['prev_image'],
|
||||
'post_image': example['post_image'],
|
||||
'changes': example['changes'],
|
||||
'prev_code_files': example['prev_code_files'],
|
||||
'post_code_files': example['post_code_files'],
|
||||
'skip': skip,
|
||||
}
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def prepare_visualcodebench(dataset):
|
||||
logger.info('Processing dataset')
|
||||
dataset_processed = []
|
||||
for example in tqdm(dataset['train']):
|
||||
formatted_example = format_task_dict(example)
|
||||
if formatted_example['skip']:
|
||||
continue
|
||||
del formatted_example['skip']
|
||||
dataset_processed.append(formatted_example)
|
||||
|
||||
return pd.DataFrame(dataset_processed)
|
||||
|
||||
|
||||
def pil_image_to_base64(image: PngImagePlugin.PngImageFile) -> str:
|
||||
"""
|
||||
Converts a PIL image to a Base64-encoded string.
|
||||
|
||||
Parameters:
|
||||
- image (PngImagePlugin.PngImageFile): The PIL image to convert.
|
||||
|
||||
Returns:
|
||||
- str: The Base64-encoded string of the image.
|
||||
"""
|
||||
if not isinstance(image, PngImagePlugin.PngImageFile):
|
||||
raise ValueError(
|
||||
'The provided image is not a PIL.PngImagePlugin.PngImageFile instance.'
|
||||
)
|
||||
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format='PNG')
|
||||
img_bytes = buffered.getvalue()
|
||||
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||
base64_with_prefix = f'data:image/png;base64,{img_base64}'
|
||||
return [base64_with_prefix]
|
||||
@@ -1,247 +0,0 @@
|
||||
# FILE: run_infer.py
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from functools import partial
|
||||
|
||||
import pandas as pd
|
||||
from datasets import load_dataset
|
||||
|
||||
# from evaluation.benchmarks.visualcodebench.eval import capture_screenshot
|
||||
from evaluation.benchmarks.visualcodebench.prepare import (
|
||||
REPO_DOWNLOAD_DIR,
|
||||
download_repository,
|
||||
pil_image_to_base64,
|
||||
prepare_visualcodebench,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
)
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
)
|
||||
from openhands.core.config.utils import parse_arguments
|
||||
from openhands.core.logger import openhands_logger as logger # Import OpenHands logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action.commands import CmdRunAction
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.observation.commands import CmdOutputObservation
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
|
||||
# Define workspace and output directories
|
||||
WORKSPACE_DIR = './workspace'
|
||||
|
||||
FAKE_RESPONSES = {
|
||||
'CodeActAgent': partial(codeact_user_response, encapsulate_solution=True),
|
||||
}
|
||||
|
||||
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='eventstream',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
return config
|
||||
|
||||
|
||||
def initialize_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required
|
||||
):
|
||||
"""Initialize the runtime for the agent.
|
||||
|
||||
This function is called before the runtime is used to run the agent.
|
||||
"""
|
||||
logger.info('-' * 30)
|
||||
logger.info('BEGIN Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
workspace_dir_name = instance['instance_id']
|
||||
obs: CmdOutputObservation
|
||||
|
||||
action = CmdRunAction(command='mkdir -p /workspace/{workspace_dir_name}')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to create /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
file_path = REPO_DOWNLOAD_DIR + f'data/{workspace_dir_name}/prev/index.html'
|
||||
runtime.copy_to(file_path, f'/workspace/{workspace_dir_name}')
|
||||
logger.info(f'Copied code file for instance {workspace_dir_name}')
|
||||
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
|
||||
|
||||
def complete_runtime(
|
||||
runtime: Runtime,
|
||||
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
|
||||
) -> str:
|
||||
# TODO: extract edited HTML file from agent workspace
|
||||
# temp_zip = runtime.copy_from(f'/workspace/{instance.instance_id}')
|
||||
# file_name = f'/workspace/{instance.instance_id}/index.html'
|
||||
# with zipfile.ZipFile(temp_zip, 'r') as zip_ref:
|
||||
# if file_name in zip_ref.namelist():
|
||||
# with zip_ref.open(file_name) as file:
|
||||
# file_content = file.read().decode('utf-8') # Decode bytes to string
|
||||
# else:
|
||||
# raise FileNotFoundError(f"'{file_name}' not found in the ZIP archive.")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_folder = REPO_DOWNLOAD_DIR + f'data/{instance.instance_id}/post/'
|
||||
shutil.copytree(src_folder, tmpdir, dirs_exist_ok=True)
|
||||
|
||||
# image = capture_screenshot(tmpdir)
|
||||
# if image is not None:
|
||||
# shutil.copy(os.path.join(tmpdir, 'final_screenshot.png'), REPO_DOWNLOAD_DIR)
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series, metadata: EvalMetadata, reset_logger: bool = True
|
||||
):
|
||||
config = get_config(metadata)
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
|
||||
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
|
||||
else:
|
||||
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
|
||||
|
||||
# =============================================
|
||||
# build instruction
|
||||
# =============================================
|
||||
|
||||
# Prepare instruction
|
||||
instruction = (
|
||||
f"Modify the HTML/CSS according to the following instruction:\n\n"
|
||||
f"{instance['changes']}\n\n"
|
||||
)
|
||||
instruction += (
|
||||
'IMPORTANT: You should ONLY interact with the environment provided '
|
||||
'to you AND NEVER ASK FOR HUMAN HELP.\n'
|
||||
)
|
||||
|
||||
# =============================================
|
||||
# create sandbox and run the agent
|
||||
# =============================================
|
||||
|
||||
runtime: Runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
try:
|
||||
initialize_runtime(runtime, instance=instance)
|
||||
|
||||
image_urls = pil_image_to_base64(instance['prev_image'])
|
||||
|
||||
action = MessageAction(content=instruction, image_urls=image_urls)
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=action,
|
||||
runtime=runtime,
|
||||
fake_user_response_fn=FAKE_RESPONSES[metadata.agent_class],
|
||||
)
|
||||
)
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
# =============================================
|
||||
# result evaluation
|
||||
# =============================================
|
||||
|
||||
return_val = complete_runtime(runtime, instance)
|
||||
logger.info(f'Return value {return_val}')
|
||||
finally:
|
||||
runtime.close()
|
||||
|
||||
# TODO: return EVAL output
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the evaluation."""
|
||||
# args = parse_args()
|
||||
args = parse_arguments()
|
||||
|
||||
logger.info(f"\n{'='*80}\nStarting VisualCodeBench Evaluation\n{'='*80}")
|
||||
logger.info(f'Agent: {args.agent_cls}')
|
||||
logger.info(f'Model: {args.llm_config}')
|
||||
logger.info(f'Max iterations: {args.max_iterations}')
|
||||
logger.info(f'Eval limit: {args.eval_n_limit}')
|
||||
logger.info(f'Num workers: {args.eval_num_workers}\n')
|
||||
logger.info(f'Eval output: {args.eval_output_dir}\n')
|
||||
|
||||
# Step 1: Download the entire repository once
|
||||
logger.info('Downloading repository...')
|
||||
download_repository()
|
||||
|
||||
# Step 2: Load Dataset
|
||||
logger.info('Loading dataset...')
|
||||
dataset = load_dataset(REPO_DOWNLOAD_DIR)
|
||||
|
||||
# Step 3: Prepare dataset
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
if llm_config is None:
|
||||
logger.error(f'Could not find LLM config: {args.llm_config}')
|
||||
raise ValueError(f'Could not find LLM config: {args.llm_config}')
|
||||
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
'VisualCodeBench',
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
'evaluation/output/',
|
||||
)
|
||||
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
dataset = prepare_visualcodebench(dataset)
|
||||
instances = prepare_dataset(dataset, output_file, eval_n_limit=args.eval_n_limit)
|
||||
|
||||
# Step 4: Run eval
|
||||
run_evaluation(
|
||||
instances, metadata, output_file, args.eval_num_workers, process_instance
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
# Check if required arguments are provided
|
||||
if [ "$#" -lt 4 ]; then
|
||||
echo "Usage: $0 [model_config] [commit_hash] [agent_cls] [eval_limit] [num_workers]"
|
||||
echo "Example: $0 llm.eval_gpt_4o_mini HEAD CodeActAgent 5 1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODEL_CONFIG=$1
|
||||
COMMIT_HASH=$2
|
||||
AGENT_CLS=$3
|
||||
EVAL_LIMIT=$4
|
||||
NUM_WORKERS=${5:-1} # Default to 1 worker if not specified
|
||||
|
||||
# Checkout the specified commit
|
||||
checkout_eval_branch
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
get_openhands_version
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
|
||||
COMMAND="export PYTHONPATH=evaluation/benchmarks/visualcodebench:\$PYTHONPATH && poetry run python evaluation/benchmarks/visualcodebench/run_infer.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations 5 \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $OPENHANDS_VERSION" \
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
@@ -1,167 +0,0 @@
|
||||
import http
|
||||
import os
|
||||
import socket
|
||||
import socketserver
|
||||
import threading
|
||||
import time
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from PIL import Image, ImageChops
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
def get_free_port():
|
||||
"""Find a free port to run the HTTP server."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('', 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def start_http_server(tmpdir):
|
||||
port = get_free_port()
|
||||
|
||||
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def translate_path(self, path):
|
||||
# Serve files from the specified directory instead of the current working directory
|
||||
path = super().translate_path(path)
|
||||
relative_path = os.path.relpath(path, os.getcwd())
|
||||
return os.path.join(tmpdir, relative_path)
|
||||
|
||||
handler = CustomHTTPRequestHandler
|
||||
server = socketserver.TCPServer(('', port), handler)
|
||||
return server, port
|
||||
|
||||
|
||||
def capture_screenshot(tmpdir):
|
||||
server, port = start_http_server(tmpdir)
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
time.sleep(10)
|
||||
|
||||
image = None
|
||||
try:
|
||||
server_url = f'http://localhost:{port}/'
|
||||
|
||||
if not is_server_reachable(server_url):
|
||||
raise RuntimeError(f'Server not reachable at {server_url}')
|
||||
|
||||
screenshot_path = os.path.join(tmpdir, 'final_screenshot.png')
|
||||
capture_screenshot_playwright(server_url, screenshot_path)
|
||||
image = Image.open(screenshot_path)
|
||||
image.load()
|
||||
finally:
|
||||
# Shut down the server and clean up
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def is_server_reachable(url):
|
||||
"""
|
||||
Check if the local server is reachable.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(url, timeout=5) # Set a 5-second timeout
|
||||
if response.status_code == 200:
|
||||
logger.info(f'Server is reachable at {url}')
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
f'Server responded with status code {response.status_code} at {url}'
|
||||
)
|
||||
return False
|
||||
except requests.ConnectionError as e:
|
||||
logger.error(f'Failed to connect to server at {url}: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def capture_screenshot_playwright(url, screenshot_path):
|
||||
"""Capture a screenshot of the given URL using Playwright."""
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
logger.info('Launching browser...')
|
||||
browser = p.chromium.launch(timeout=10000) # 10 seconds for browser launch
|
||||
|
||||
logger.info('Creating a new page...')
|
||||
page = browser.new_page()
|
||||
|
||||
logger.info(f'Navigating to URL: {url}')
|
||||
try:
|
||||
page.goto(url, timeout=60 * 1000) # Set timeout to 5 seconds
|
||||
logger.info('Page navigation completed.')
|
||||
except Exception as e:
|
||||
logger.warning(f'Page navigation timed out. {e}. Continuing...')
|
||||
|
||||
logger.info('Waiting for network to be idle...')
|
||||
try:
|
||||
page.wait_for_load_state(
|
||||
'networkidle', timeout=60 * 1000
|
||||
) # Set timeout to 5 seconds
|
||||
logger.info('Page load state reached.')
|
||||
except Exception as e:
|
||||
logger.warning(f'Page load state timed out. {e}. Continuing...')
|
||||
|
||||
logger.info('Capturing screenshot...')
|
||||
page.screenshot(
|
||||
path=screenshot_path, full_page=True
|
||||
) # Capture full page screenshot
|
||||
|
||||
logger.info(f'Screenshot saved to {screenshot_path}')
|
||||
browser.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f'Error capturing screenshot with Playwright: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def evaluate(task, screenshot_path):
|
||||
"""Compare generated screenshot with post_image using CLIP score."""
|
||||
try:
|
||||
import torch
|
||||
from transformers import CLIPModel, CLIPProcessor
|
||||
|
||||
# Load CLIP model and processor
|
||||
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
|
||||
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
|
||||
|
||||
# Load images
|
||||
post_image = Image.open(BytesIO(task['post_image']))
|
||||
generated_img = Image.open(screenshot_path)
|
||||
|
||||
# Process images
|
||||
inputs = processor(
|
||||
images=[post_image, generated_img], return_tensors='pt', padding=True
|
||||
)
|
||||
|
||||
# Get image features
|
||||
image_features = model.get_image_features(**inputs)
|
||||
|
||||
# Calculate cosine similarity
|
||||
similarity = torch.nn.functional.cosine_similarity(
|
||||
image_features[0].unsqueeze(0), image_features[1].unsqueeze(0)
|
||||
).item()
|
||||
|
||||
logger.info(f'CLIP similarity score: {similarity}')
|
||||
|
||||
return similarity > 0.95 # Consider it a match if similarity > 95%
|
||||
except Exception as e:
|
||||
logger.error(f'Error in CLIP evaluation: {e}')
|
||||
# Fallback to pixel comparison if CLIP fails
|
||||
try:
|
||||
post_image = Image.open(BytesIO(task['post_image']))
|
||||
generated_img = Image.open(screenshot_path)
|
||||
|
||||
# Compare images directly without converting to bytes
|
||||
diff = ImageChops.difference(generated_img, post_image)
|
||||
logger.info(
|
||||
f"Pixel difference analysis: {'No difference' if not diff.getbbox() else 'Differences found'}"
|
||||
)
|
||||
return not diff.getbbox()
|
||||
except Exception as ex:
|
||||
logger.error(f'Error in fallback evaluation: {ex}')
|
||||
return False
|
||||
@@ -155,9 +155,7 @@ describe("Sidebar", () => {
|
||||
const settingsModal = screen.getByTestId("ai-config-modal");
|
||||
|
||||
// Click the advanced options switch to show the API key input
|
||||
const advancedOptionsSwitch = within(settingsModal).getByTestId(
|
||||
"advanced-option-switch",
|
||||
);
|
||||
const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch");
|
||||
await user.click(advancedOptionsSwitch);
|
||||
|
||||
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
|
||||
|
||||
@@ -2,6 +2,13 @@ import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { UploadImageInput } from "#/components/features/images/upload-image-input";
|
||||
import { toast } from "#/utils/toast";
|
||||
|
||||
vi.mock("#/utils/toast", () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("UploadImageInput", () => {
|
||||
const user = userEvent.setup();
|
||||
@@ -41,17 +48,37 @@ describe("UploadImageInput", () => {
|
||||
expect(onUploadMock).toHaveBeenNthCalledWith(1, files);
|
||||
});
|
||||
|
||||
it("should not upload any file that is not an image", async () => {
|
||||
it("should show error and not upload unsupported image types", async () => {
|
||||
render(<UploadImageInput onUpload={onUploadMock} />);
|
||||
|
||||
const file = new File(["(⌐□_□)"], "chucknorris.txt", {
|
||||
type: "text/plain",
|
||||
const file = new File(["(⌐□_□)"], "chucknorris.bmp", {
|
||||
type: "image/bmp",
|
||||
});
|
||||
const input = screen.getByTestId("upload-image-input");
|
||||
|
||||
await user.upload(input, file);
|
||||
|
||||
expect(onUploadMock).not.toHaveBeenCalled();
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Only JPEG, PNG, GIF, and WebP images are supported")
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle mix of supported and unsupported image types", async () => {
|
||||
render(<UploadImageInput onUpload={onUploadMock} />);
|
||||
|
||||
const files = [
|
||||
new File(["(⌐□_□)"], "valid.png", { type: "image/png" }),
|
||||
new File(["(⌐□_□)"], "invalid.bmp", { type: "image/bmp" }),
|
||||
];
|
||||
const input = screen.getByTestId("upload-image-input");
|
||||
|
||||
await user.upload(input, files);
|
||||
|
||||
expect(onUploadMock).toHaveBeenCalledWith([files[0]]);
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Only JPEG, PNG, GIF, and WebP images are supported")
|
||||
);
|
||||
});
|
||||
|
||||
it("should render custom labels", () => {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import store from "../src/store";
|
||||
import {
|
||||
setInitialPrompt,
|
||||
clearInitialPrompt,
|
||||
setInitialQuery,
|
||||
clearInitialQuery,
|
||||
} from "../src/state/initial-query-slice";
|
||||
|
||||
describe("Initial Query Behavior", () => {
|
||||
it("should clear initial query when clearInitialPrompt is dispatched", () => {
|
||||
it("should clear initial query when clearInitialQuery is dispatched", () => {
|
||||
// Set up initial query in the store
|
||||
store.dispatch(setInitialPrompt("test query"));
|
||||
expect(store.getState().initialQuery.initialPrompt).toBe("test query");
|
||||
store.dispatch(setInitialQuery("test query"));
|
||||
expect(store.getState().initialQuery.initialQuery).toBe("test query");
|
||||
|
||||
// Clear the initial query
|
||||
store.dispatch(clearInitialPrompt());
|
||||
store.dispatch(clearInitialQuery());
|
||||
|
||||
// Verify initial query is cleared
|
||||
expect(store.getState().initialQuery.initialPrompt).toBeNull();
|
||||
expect(store.getState().initialQuery.initialQuery).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { validateImageType, getValidImageFiles } from '#/utils/validate-image-type';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('validateImageType', () => {
|
||||
it('should accept supported image types', () => {
|
||||
const supportedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
supportedTypes.forEach((type) => {
|
||||
const file = new File([''], 'test.jpg', { type });
|
||||
expect(validateImageType(file)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject unsupported image types', () => {
|
||||
const unsupportedTypes = ['image/bmp', 'image/tiff', 'application/pdf', 'text/plain'];
|
||||
unsupportedTypes.forEach((type) => {
|
||||
const file = new File([''], 'test.jpg', { type });
|
||||
expect(validateImageType(file)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidImageFiles', () => {
|
||||
it('should separate valid and invalid files', () => {
|
||||
const files = [
|
||||
new File([''], 'test1.jpg', { type: 'image/jpeg' }),
|
||||
new File([''], 'test2.bmp', { type: 'image/bmp' }),
|
||||
new File([''], 'test3.png', { type: 'image/png' }),
|
||||
new File([''], 'test4.pdf', { type: 'application/pdf' }),
|
||||
];
|
||||
|
||||
const { validFiles, invalidFiles } = getValidImageFiles(files);
|
||||
|
||||
expect(validFiles).toHaveLength(2);
|
||||
expect(invalidFiles).toHaveLength(2);
|
||||
expect(validFiles[0].type).toBe('image/jpeg');
|
||||
expect(validFiles[1].type).toBe('image/png');
|
||||
expect(invalidFiles[0].type).toBe('image/bmp');
|
||||
expect(invalidFiles[1].type).toBe('application/pdf');
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const { validFiles, invalidFiles } = getValidImageFiles([]);
|
||||
expect(validFiles).toHaveLength(0);
|
||||
expect(invalidFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Generated
+3
-106
@@ -105,7 +105,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -1594,7 +1593,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@@ -1612,7 +1610,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -1625,7 +1622,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -3408,7 +3404,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -3422,7 +3417,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -3432,7 +3426,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -3533,7 +3526,6 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -5764,7 +5756,7 @@
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
@@ -5773,7 +5765,6 @@
|
||||
"version": "19.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.7.tgz",
|
||||
"integrity": "sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -6462,7 +6453,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6472,7 +6462,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -6488,14 +6477,12 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -6509,7 +6496,6 @@
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -6835,7 +6821,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
@@ -6860,7 +6845,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -6909,7 +6893,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -6919,7 +6902,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -7070,7 +7052,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -7646,7 +7627,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -7661,7 +7641,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -7684,7 +7663,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
@@ -7951,7 +7929,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
@@ -7971,7 +7948,6 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
@@ -8036,7 +8012,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
@@ -8055,7 +8030,6 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -9212,7 +9186,6 @@
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -9229,7 +9202,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -9256,7 +9228,6 @@
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -9298,7 +9269,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -9418,7 +9388,6 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
@@ -9689,7 +9658,6 @@
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@@ -9710,7 +9678,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
@@ -10462,7 +10429,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
@@ -10505,7 +10471,6 @@
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
@@ -10573,7 +10538,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10628,7 +10592,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -10681,7 +10644,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -10910,7 +10872,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
@@ -10989,7 +10950,6 @@
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -11005,7 +10965,6 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -11220,7 +11179,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -11233,7 +11191,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
@@ -12008,7 +11965,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -12589,7 +12545,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -12680,7 +12635,6 @@
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -12706,7 +12660,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -12866,7 +12819,6 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0",
|
||||
@@ -12987,7 +12939,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13098,7 +13049,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13108,7 +13058,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -13350,7 +13299,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
@@ -13469,7 +13417,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -13479,14 +13426,12 @@
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -13503,7 +13448,6 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
@@ -13559,7 +13503,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -13585,7 +13528,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13595,7 +13537,6 @@
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -13674,7 +13615,6 @@
|
||||
"version": "15.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.0.0",
|
||||
@@ -13692,7 +13632,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
|
||||
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camelcase-css": "^2.0.1"
|
||||
@@ -13712,7 +13651,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
|
||||
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13748,7 +13686,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13774,7 +13711,6 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -13802,7 +13738,6 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
@@ -14064,7 +13999,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -14313,7 +14247,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pify": "^2.3.0"
|
||||
@@ -14642,7 +14575,6 @@
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.16.0",
|
||||
@@ -14716,7 +14648,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -14841,7 +14772,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -15124,7 +15054,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -15137,7 +15066,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -15226,7 +15154,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -15546,7 +15473,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@@ -15565,7 +15491,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -15580,14 +15505,12 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -15597,7 +15520,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -15610,7 +15532,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -15753,7 +15674,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -15767,7 +15687,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -15838,7 +15757,6 @@
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
@@ -15861,7 +15779,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -15884,7 +15801,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -15962,7 +15878,6 @@
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -16000,7 +15915,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@@ -16025,7 +15939,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -16038,7 +15951,6 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -16052,7 +15964,6 @@
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
@@ -16086,7 +15997,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.0.0"
|
||||
@@ -16096,7 +16006,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"thenify": ">= 3.1.0 < 4"
|
||||
@@ -16189,7 +16098,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -16278,7 +16186,6 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tsconfck": {
|
||||
@@ -16460,7 +16367,7 @@
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16501,7 +16408,7 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
@@ -16718,7 +16625,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
@@ -17256,7 +17162,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@@ -17275,7 +17180,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -17293,14 +17197,12 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -17310,7 +17212,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -17325,7 +17226,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -17338,7 +17238,6 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -17351,7 +17250,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -17445,7 +17343,6 @@
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
|
||||
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
|
||||
@@ -244,14 +244,10 @@ class OpenHands {
|
||||
static async createConversation(
|
||||
githubToken?: string,
|
||||
selectedRepository?: string,
|
||||
initialUserMsg?: string,
|
||||
imageUrls?: string[],
|
||||
): Promise<Conversation> {
|
||||
const body = {
|
||||
github_token: githubToken,
|
||||
selected_repository: selectedRepository,
|
||||
initial_user_msg: initialUserMsg,
|
||||
image_urls: imageUrls,
|
||||
};
|
||||
|
||||
const { data } = await openHands.post<Conversation>(
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AGENT_STATUS_MAP: {
|
||||
},
|
||||
[AgentState.AWAITING_USER_INPUT]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE,
|
||||
indicator: IndicatorColor.BLUE,
|
||||
indicator: IndicatorColor.ORANGE,
|
||||
},
|
||||
[AgentState.PAUSED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { SubmitButton } from "#/components/shared/buttons/submit-button";
|
||||
import { StopButton } from "#/components/shared/buttons/stop-button";
|
||||
import { getValidImageFiles } from "#/utils/validate-image-type";
|
||||
import { toast } from "#/utils/toast";
|
||||
|
||||
interface ChatInputProps {
|
||||
name?: string;
|
||||
@@ -46,13 +48,22 @@ export function ChatInput({
|
||||
const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
// Only handle paste if we have an image paste handler and there are files
|
||||
if (onImagePaste && event.clipboardData.files.length > 0) {
|
||||
const files = Array.from(event.clipboardData.files).filter((file) =>
|
||||
file.type.startsWith("image/"),
|
||||
);
|
||||
// Only prevent default if we found image files to handle
|
||||
if (files.length > 0) {
|
||||
const files = Array.from(event.clipboardData.files);
|
||||
const { validFiles, invalidFiles } = getValidImageFiles(files);
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
toast.error(
|
||||
t(I18nKey.UPLOAD$UNSUPPORTED_IMAGE_TYPE, {
|
||||
count: invalidFiles.length,
|
||||
files: invalidFiles.map((f) => f.name).join(", "),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Only prevent default if we found valid image files to handle
|
||||
if (validFiles.length > 0) {
|
||||
event.preventDefault();
|
||||
onImagePaste(files);
|
||||
onImagePaste(validFiles);
|
||||
}
|
||||
}
|
||||
// For text paste, let the default behavior handle it
|
||||
@@ -74,11 +85,20 @@ export function ChatInput({
|
||||
event.preventDefault();
|
||||
setIsDraggingOver(false);
|
||||
if (onImagePaste && event.dataTransfer.files.length > 0) {
|
||||
const files = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
file.type.startsWith("image/"),
|
||||
);
|
||||
if (files.length > 0) {
|
||||
onImagePaste(files);
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
const { validFiles, invalidFiles } = getValidImageFiles(files);
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
toast.error(
|
||||
t(I18nKey.UPLOAD$UNSUPPORTED_IMAGE_TYPE, {
|
||||
count: invalidFiles.length,
|
||||
files: invalidFiles.map((f) => f.name).join(", "),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onImagePaste(validFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Clip from "#/icons/clip.svg?react";
|
||||
import { getValidImageFiles } from "#/utils/validate-image-type";
|
||||
import { toast } from "#/utils/toast";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface UploadImageInputProps {
|
||||
onUpload: (files: File[]) => void;
|
||||
@@ -6,8 +10,26 @@ interface UploadImageInputProps {
|
||||
}
|
||||
|
||||
export function UploadImageInput({ onUpload, label }: UploadImageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files) onUpload(Array.from(event.target.files));
|
||||
if (!event.target.files) return;
|
||||
|
||||
const files = Array.from(event.target.files);
|
||||
const { validFiles, invalidFiles } = getValidImageFiles(files);
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
toast.error(
|
||||
t(I18nKey.UPLOAD$UNSUPPORTED_IMAGE_TYPE, {
|
||||
count: invalidFiles.length,
|
||||
files: invalidFiles.map((f) => f.name).join(", "),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onUpload(validFiles);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -16,7 +38,7 @@ export function UploadImageInput({ onUpload, label }: UploadImageInputProps) {
|
||||
<input
|
||||
data-testid="upload-image-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
multiple
|
||||
hidden
|
||||
onChange={handleUpload}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router";
|
||||
import posthog from "posthog-js";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { setInitialQuery } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useCreateConversation = () => {
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { q?: string }) => {
|
||||
mutationFn: (variables: { q?: string }) => {
|
||||
if (
|
||||
!variables.q?.trim() &&
|
||||
!selectedRepository &&
|
||||
@@ -28,13 +28,10 @@ export const useCreateConversation = () => {
|
||||
throw new Error("No query provided");
|
||||
}
|
||||
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
|
||||
if (variables.q) dispatch(setInitialQuery(variables.q));
|
||||
return OpenHands.createConversation(
|
||||
gitHubToken || undefined,
|
||||
selectedRepository || undefined,
|
||||
variables.q,
|
||||
files,
|
||||
);
|
||||
},
|
||||
onSuccess: async ({ conversation_id: conversationId }, { q }) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useGitHubUser = () => {
|
||||
const { gitHubToken, setUserId, logout } = useAuth();
|
||||
const { gitHubToken, setUserId } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const user = useQuery({
|
||||
@@ -29,11 +29,5 @@ export const useGitHubUser = () => {
|
||||
}
|
||||
}, [user.data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user.isError) {
|
||||
logout();
|
||||
}
|
||||
}, [user.isError]);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
@@ -419,6 +419,22 @@
|
||||
"ja": "コードエディタ",
|
||||
"tr": "Kod Editörü"
|
||||
},
|
||||
"UPLOAD$UNSUPPORTED_IMAGE_TYPE": {
|
||||
"en": "Only JPEG, PNG, GIF, and WebP images are supported",
|
||||
"zh-CN": "仅支持 JPEG、PNG、GIF 和 WebP 图片",
|
||||
"de": "Nur JPEG-, PNG-, GIF- und WebP-Bilder werden unterstützt",
|
||||
"ko-KR": "JPEG, PNG, GIF 및 WebP 이미지만 지원됩니다",
|
||||
"no": "Kun JPEG, PNG, GIF og WebP bilder støttes",
|
||||
"zh-TW": "僅支援 JPEG、PNG、GIF 和 WebP 圖片",
|
||||
"ar": "يتم دعم صور JPEG و PNG و GIF و WebP فقط",
|
||||
"fr": "Seules les images JPEG, PNG, GIF et WebP sont prises en charge",
|
||||
"it": "Sono supportate solo immagini JPEG, PNG, GIF e WebP",
|
||||
"pt": "Apenas imagens JPEG, PNG, GIF e WebP são suportadas",
|
||||
"es": "Solo se admiten imágenes JPEG, PNG, GIF y WebP",
|
||||
"ja": "JPEG、PNG、GIF、WebP画像のみがサポートされています",
|
||||
"tr": "Yalnızca JPEG, PNG, GIF ve WebP görüntüleri desteklenir"
|
||||
},
|
||||
|
||||
"WORKSPACE$BROWSER_TAB_LABEL": {
|
||||
"en": "Browser",
|
||||
"zh-CN": "浏览器",
|
||||
|
||||
@@ -141,7 +141,7 @@ export const handlers = [
|
||||
{ id: 2, full_name: "octocat/earth" },
|
||||
]),
|
||||
),
|
||||
http.get("/api/github/user", () => {
|
||||
http.get("https://api.github.com/user", () => {
|
||||
const user: GitHubUser = {
|
||||
id: 1,
|
||||
login: "octocat",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import { useWSStatusChange } from "./hooks/use-ws-status-change";
|
||||
import { useHandleWSEvents } from "./hooks/use-handle-ws-events";
|
||||
import { useHandleRuntimeActive } from "./hooks/use-handle-runtime-active";
|
||||
|
||||
export function EventHandler({ children }: React.PropsWithChildren) {
|
||||
useWSStatusChange();
|
||||
useHandleWSEvents();
|
||||
useHandleRuntimeActive();
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { addUserMessage } from "#/state/chat-slice";
|
||||
import { clearFiles, clearInitialQuery } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
export const useWSStatusChange = () => {
|
||||
const { send, status } = useWsClient();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const statusRef = React.useRef<WsClientProviderStatus | null>(null);
|
||||
|
||||
const { files, initialQuery } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
|
||||
const sendInitialQuery = (query: string, base64Files: string[]) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
send(createChatMessage(query, base64Files, timestamp));
|
||||
};
|
||||
|
||||
const dispatchInitialQuery = (query: string) => {
|
||||
sendInitialQuery(query, files);
|
||||
dispatch(clearFiles()); // reset selected files
|
||||
dispatch(clearInitialQuery()); // reset initial query
|
||||
};
|
||||
|
||||
const handleAgentInit = () => {
|
||||
if (initialQuery) {
|
||||
dispatchInitialQuery(initialQuery);
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
if (curAgentState === AgentState.INIT) {
|
||||
handleAgentInit();
|
||||
}
|
||||
}, [curAgentState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (statusRef.current === status) {
|
||||
return; // This is a check because of strict mode - if the status did not change, don't do anything
|
||||
}
|
||||
statusRef.current = status;
|
||||
|
||||
if (status !== WsClientProviderStatus.DISCONNECTED && initialQuery) {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: initialQuery,
|
||||
imageUrls: files,
|
||||
timestamp: new Date().toISOString(),
|
||||
pending: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
dispatch(setCurrentAgentState(AgentState.STOPPED));
|
||||
}
|
||||
}, [status]);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { FaServer } from "react-icons/fa";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useConversation,
|
||||
} from "#/context/conversation-context";
|
||||
import { Controls } from "#/components/features/controls/controls";
|
||||
import { clearMessages, addUserMessage } from "#/state/chat-slice";
|
||||
import { clearMessages } from "#/state/chat-slice";
|
||||
import { clearTerminal } from "#/state/command-slice";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
import CodeIcon from "#/icons/code.svg?react";
|
||||
@@ -36,8 +36,6 @@ import { ServedAppLabel } from "#/components/layout/served-app-label";
|
||||
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@@ -48,9 +46,6 @@ function AppContent() {
|
||||
const { data: conversation, isFetched } = useUserConversation(
|
||||
conversationId || null,
|
||||
);
|
||||
const { initialPrompt, files } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
|
||||
@@ -79,18 +74,6 @@ function AppContent() {
|
||||
dispatch(clearMessages());
|
||||
dispatch(clearTerminal());
|
||||
dispatch(clearJupyter());
|
||||
if (conversationId && (initialPrompt || files.length > 0)) {
|
||||
dispatch(
|
||||
addUserMessage({
|
||||
content: initialPrompt || "",
|
||||
imageUrls: files || [],
|
||||
timestamp: new Date().toISOString(),
|
||||
pending: true,
|
||||
}),
|
||||
);
|
||||
dispatch(clearInitialPrompt());
|
||||
dispatch(clearFiles());
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
|
||||
@@ -2,14 +2,14 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
type SliceState = {
|
||||
files: string[]; // base64 encoded images
|
||||
initialPrompt: string | null;
|
||||
initialQuery: string | null;
|
||||
selectedRepository: string | null;
|
||||
importedProjectZip: string | null; // base64 encoded zip
|
||||
};
|
||||
|
||||
const initialState: SliceState = {
|
||||
files: [],
|
||||
initialPrompt: null,
|
||||
initialQuery: null,
|
||||
selectedRepository: null,
|
||||
importedProjectZip: null,
|
||||
};
|
||||
@@ -27,11 +27,11 @@ export const selectedFilesSlice = createSlice({
|
||||
clearFiles(state) {
|
||||
state.files = [];
|
||||
},
|
||||
setInitialPrompt(state, action: PayloadAction<string>) {
|
||||
state.initialPrompt = action.payload;
|
||||
setInitialQuery(state, action: PayloadAction<string>) {
|
||||
state.initialQuery = action.payload;
|
||||
},
|
||||
clearInitialPrompt(state) {
|
||||
state.initialPrompt = null;
|
||||
clearInitialQuery(state) {
|
||||
state.initialQuery = null;
|
||||
},
|
||||
setSelectedRepository(state, action: PayloadAction<string | null>) {
|
||||
state.selectedRepository = action.payload;
|
||||
@@ -49,8 +49,8 @@ export const {
|
||||
addFile,
|
||||
removeFile,
|
||||
clearFiles,
|
||||
setInitialPrompt,
|
||||
clearInitialPrompt,
|
||||
setInitialQuery,
|
||||
clearInitialQuery,
|
||||
setSelectedRepository,
|
||||
clearSelectedRepository,
|
||||
setImportedProjectZip,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const SUPPORTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] as const;
|
||||
|
||||
export function validateImageType(file: File): boolean {
|
||||
return SUPPORTED_IMAGE_TYPES.includes(file.type as typeof SUPPORTED_IMAGE_TYPES[number]);
|
||||
}
|
||||
|
||||
export function getValidImageFiles(files: File[]): { validFiles: File[]; invalidFiles: File[] } {
|
||||
const validFiles: File[] = [];
|
||||
const invalidFiles: File[] = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (validateImageType(file)) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
invalidFiles.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
return { validFiles, invalidFiles };
|
||||
}
|
||||
@@ -32,7 +32,6 @@ from openhands.events.tool import ToolCallMetadata
|
||||
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
|
||||
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
|
||||
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
|
||||
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.
|
||||
"""
|
||||
|
||||
CmdRunTool = ChatCompletionToolParam(
|
||||
@@ -45,7 +44,7 @@ CmdRunTool = ChatCompletionToolParam(
|
||||
'properties': {
|
||||
'command': {
|
||||
'type': 'string',
|
||||
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.',
|
||||
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process.',
|
||||
},
|
||||
'is_input': {
|
||||
'type': 'string',
|
||||
|
||||
@@ -501,6 +501,10 @@ class AgentController:
|
||||
EventSource.ENVIRONMENT,
|
||||
)
|
||||
|
||||
if new_state == AgentState.INIT and self.state.resume_state:
|
||||
await self.set_agent_state_to(self.state.resume_state)
|
||||
self.state.resume_state = None
|
||||
|
||||
def get_agent_state(self) -> AgentState:
|
||||
"""Returns the current state of the agent.
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ __all__ = ['ActionType']
|
||||
|
||||
|
||||
class ActionTypeSchema(BaseModel):
|
||||
INIT: str = Field(default='initialize')
|
||||
"""Initializes the agent. Only sent by client.
|
||||
"""
|
||||
|
||||
MESSAGE: str = Field(default='message')
|
||||
"""Represents a message.
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,10 @@ class AgentState(str, Enum):
|
||||
"""The agent is loading.
|
||||
"""
|
||||
|
||||
INIT = 'init'
|
||||
"""The agent is initialized.
|
||||
"""
|
||||
|
||||
RUNNING = 'running'
|
||||
"""The agent is running.
|
||||
"""
|
||||
|
||||
@@ -567,6 +567,12 @@ def main():
|
||||
choices=['issue', 'pr'],
|
||||
help='Type of issue to resolve, either open issue or pr comments.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target-branch',
|
||||
type=str,
|
||||
default=None,
|
||||
help="Target branch to pull and create PR against (for PRs). If not specified, uses the PR's base branch.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--is-experimental',
|
||||
type=lambda x: x.lower() == 'true',
|
||||
|
||||
@@ -306,12 +306,6 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
assert 'pod_status' in runtime_data
|
||||
pod_status = runtime_data['pod_status'].lower()
|
||||
self.log('debug', f'Pod status: {pod_status}')
|
||||
restart_count = runtime_data.get('restart_count', 0)
|
||||
if restart_count != 0:
|
||||
restart_reasons = runtime_data.get('restart_reasons')
|
||||
self.log(
|
||||
'debug', f'Pod restarts: {restart_count}, reasons: {restart_reasons}'
|
||||
)
|
||||
|
||||
# FIXME: We should fix it at the backend of /start endpoint, make sure
|
||||
# the pod is created before returning the response.
|
||||
|
||||
@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
|
||||
import socketio
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.server.settings import Settings
|
||||
@@ -69,7 +68,7 @@ class ConversationManager(ABC):
|
||||
sid: str,
|
||||
settings: Settings,
|
||||
user_id: str | None,
|
||||
initial_user_msg: MessageAction | None = None,
|
||||
initial_user_msg: str | None = None,
|
||||
) -> EventStream:
|
||||
"""Start an event loop if one is not already running"""
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from openhands.core.config.app_config import AppConfig
|
||||
from openhands.core.exceptions import AgentRuntimeUnavailableError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.stream import EventStream, session_exists
|
||||
from openhands.server.session.conversation import Conversation
|
||||
from openhands.server.session.session import ROOM_KEY, Session
|
||||
@@ -187,7 +186,7 @@ class StandaloneConversationManager(ConversationManager):
|
||||
sid: str,
|
||||
settings: Settings,
|
||||
user_id: str | None,
|
||||
initial_user_msg: MessageAction | None = None,
|
||||
initial_user_msg: str | None = None,
|
||||
) -> EventStream:
|
||||
logger.info(f'maybe_start_agent_loop:{sid}')
|
||||
session: Session | None = None
|
||||
|
||||
@@ -5,6 +5,7 @@ from pydantic import SecretStr
|
||||
from socketio.exceptions import ConnectionRefusedError
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import (
|
||||
NullAction,
|
||||
)
|
||||
@@ -85,6 +86,8 @@ async def connect(connection_id: str, environ, auth):
|
||||
):
|
||||
continue
|
||||
elif isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state == AgentState.INIT:
|
||||
await sio.emit('oh_event', event_to_dict(event), to=connection_id)
|
||||
agent_state_changed = event
|
||||
else:
|
||||
await sio.emit('oh_event', event_to_dict(event), to=connection_id)
|
||||
|
||||
@@ -2,6 +2,7 @@ import uvicorn
|
||||
from fastapi import FastAPI, WebSocket
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema import ActionType
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
app = FastAPI()
|
||||
@@ -10,6 +11,10 @@ app = FastAPI()
|
||||
@app.websocket('/ws')
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
# send message to mock connection
|
||||
await websocket.send_json(
|
||||
{'action': ActionType.INIT, 'message': 'Control loop started.'}
|
||||
)
|
||||
|
||||
try:
|
||||
while should_continue():
|
||||
|
||||
@@ -7,7 +7,6 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.stream import EventStreamSubscriber
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.server.auth import get_user_id
|
||||
@@ -35,7 +34,6 @@ class InitSessionRequest(BaseModel):
|
||||
github_token: str | None = None
|
||||
selected_repository: str | None = None
|
||||
initial_user_msg: str | None = None
|
||||
image_urls: list[str] | None = None
|
||||
|
||||
|
||||
async def _create_new_conversation(
|
||||
@@ -43,7 +41,6 @@ async def _create_new_conversation(
|
||||
token: str | None,
|
||||
selected_repository: str | None,
|
||||
initial_user_msg: str | None,
|
||||
image_urls: list[str] | None,
|
||||
):
|
||||
logger.info('Loading settings')
|
||||
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
|
||||
@@ -97,14 +94,8 @@ async def _create_new_conversation(
|
||||
)
|
||||
|
||||
logger.info(f'Starting agent loop for conversation {conversation_id}')
|
||||
initial_message_action = None
|
||||
if initial_user_msg or image_urls:
|
||||
initial_message_action = MessageAction(
|
||||
content=initial_user_msg or '',
|
||||
image_urls=image_urls or [],
|
||||
)
|
||||
event_stream = await conversation_manager.maybe_start_agent_loop(
|
||||
conversation_id, conversation_init_data, user_id, initial_message_action
|
||||
conversation_id, conversation_init_data, user_id, initial_user_msg
|
||||
)
|
||||
try:
|
||||
event_stream.subscribe(
|
||||
@@ -130,16 +121,10 @@ async def new_conversation(request: Request, data: InitSessionRequest):
|
||||
github_token = getattr(request.state, 'github_token', '') or data.github_token
|
||||
selected_repository = data.selected_repository
|
||||
initial_user_msg = data.initial_user_msg
|
||||
image_urls = data.image_urls or []
|
||||
|
||||
try:
|
||||
# Create conversation with initial message
|
||||
conversation_id = await _create_new_conversation(
|
||||
user_id,
|
||||
github_token,
|
||||
selected_repository,
|
||||
initial_user_msg,
|
||||
image_urls,
|
||||
user_id, github_token, selected_repository, initial_user_msg
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
|
||||
@@ -8,12 +8,19 @@ interruptions are recoverable.
|
||||
There are 3 main server side event handlers:
|
||||
|
||||
* `connect` - Invoked when a new connection to the server is established. (This may be via http or WebSocket)
|
||||
* `oh_action` - Invoked when a connected client sends an event (such as a prompt for the Agent) -
|
||||
* `oh_action` - Invoked when a connected client sends an event (Such as `INIT` or a prompt for the Agent) -
|
||||
this is distinct from the `oh_event` sent from the server to the client.
|
||||
* `disconnect` - Invoked when a connected client disconnects from the server.
|
||||
|
||||
## Init
|
||||
Each connection has a unique id, and when initially established, is not associated with any session. An
|
||||
`INIT` event must be sent to the server in order to attach a connection to a session. The `INIT` event
|
||||
may optionally include a GitHub token and a token to connect to an existing session. (Which may be running
|
||||
locally or may need to be hydrated). If no token is received as part of the init event, it is assumed a
|
||||
new session should be started.
|
||||
|
||||
## Disconnect
|
||||
The (manager)[manager.py] manages connections and sessions. Each session may have zero or more connections
|
||||
associated with it. When a session no longer has any
|
||||
associated with it, managed by invocations of `INIT` and disconnect. When a session no longer has any
|
||||
connections associated with it, after a set amount of time (determined by `config.sandbox.close_delay`),
|
||||
the session and runtime are passivated (So will need to be rehydrated to continue.)
|
||||
|
||||
@@ -9,7 +9,8 @@ from openhands.core.config import AgentConfig, AppConfig, LLMConfig
|
||||
from openhands.core.exceptions import AgentRuntimeUnavailableError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import ChangeAgentStateAction, MessageAction
|
||||
from openhands.events.action import ChangeAgentStateAction
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.microagent import BaseMicroAgent
|
||||
@@ -71,7 +72,7 @@ class AgentSession:
|
||||
agent_configs: dict[str, AgentConfig] | None = None,
|
||||
github_token: str | None = None,
|
||||
selected_repository: str | None = None,
|
||||
initial_message: MessageAction | None = None,
|
||||
initial_user_msg: str | None = None,
|
||||
):
|
||||
"""Starts the Agent session
|
||||
Parameters:
|
||||
@@ -110,17 +111,15 @@ class AgentSession:
|
||||
agent_to_llm_config=agent_to_llm_config,
|
||||
agent_configs=agent_configs,
|
||||
)
|
||||
if initial_message:
|
||||
self.event_stream.add_event(initial_message, EventSource.USER)
|
||||
self.event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.RUNNING), EventSource.ENVIRONMENT
|
||||
)
|
||||
else:
|
||||
self.event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.AWAITING_USER_INPUT),
|
||||
EventSource.ENVIRONMENT,
|
||||
)
|
||||
self.event_stream.add_event(
|
||||
ChangeAgentStateAction(AgentState.INIT), EventSource.ENVIRONMENT
|
||||
)
|
||||
|
||||
if initial_user_msg:
|
||||
self.event_stream.add_event(
|
||||
MessageAction(content=initial_user_msg), EventSource.USER
|
||||
)
|
||||
|
||||
self._starting = False
|
||||
|
||||
async def close(self):
|
||||
|
||||
@@ -11,7 +11,6 @@ from openhands.core.config import AppConfig
|
||||
from openhands.core.exceptions import AgentRuntimeUnavailableError
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.stream import EventStream, session_exists
|
||||
from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE
|
||||
from openhands.server.session.conversation import Conversation
|
||||
@@ -447,7 +446,7 @@ class SessionManager:
|
||||
sid: str,
|
||||
settings: Settings,
|
||||
user_id: str | None,
|
||||
initial_message: MessageAction | None = None,
|
||||
initial_user_msg: str | None = None,
|
||||
) -> EventStream:
|
||||
logger.info(f'maybe_start_agent_loop:{sid}')
|
||||
session: Session | None = None
|
||||
@@ -470,7 +469,7 @@ class SessionManager:
|
||||
user_id=user_id,
|
||||
)
|
||||
self._local_agent_loops_by_sid[sid] = session
|
||||
asyncio.create_task(session.initialize_agent(settings, initial_message))
|
||||
asyncio.create_task(session.initialize_agent(settings, initial_user_msg))
|
||||
|
||||
event_stream = await self._get_event_stream(sid)
|
||||
if not event_stream:
|
||||
|
||||
@@ -74,9 +74,7 @@ class Session:
|
||||
self.is_alive = False
|
||||
await self.agent_session.close()
|
||||
|
||||
async def initialize_agent(
|
||||
self, settings: Settings, initial_message: MessageAction | None
|
||||
):
|
||||
async def initialize_agent(self, settings: Settings, initial_user_msg: str | None):
|
||||
self.agent_session.event_stream.add_event(
|
||||
AgentStateChangedObservation('', AgentState.LOADING),
|
||||
EventSource.ENVIRONMENT,
|
||||
@@ -124,7 +122,7 @@ class Session:
|
||||
agent_configs=self.config.get_agent_configs(),
|
||||
github_token=github_token,
|
||||
selected_repository=selected_repository,
|
||||
initial_message=initial_message,
|
||||
initial_user_msg=initial_user_msg,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f'Error creating agent_session: {e}')
|
||||
|
||||
Generated
+21
-32
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -1108,20 +1108,6 @@ humanfriendly = ">=9.1"
|
||||
[package.extras]
|
||||
cron = ["capturer (>=2.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "colormath"
|
||||
version = "3.0.0"
|
||||
description = "Color math and conversion library."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "colormath-3.0.0.tar.gz", hash = "sha256:3d4605af344527da0e4f9f504fad7ddbebda35322c566a6c72e28edb1ff31217"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
networkx = ">=2.0"
|
||||
numpy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "comm"
|
||||
version = "0.2.2"
|
||||
@@ -3958,19 +3944,19 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index"
|
||||
version = "0.12.13"
|
||||
version = "0.12.12"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index-0.12.13-py3-none-any.whl", hash = "sha256:0b285aa451ced6bd8da40df99068ac96badf8b5725c4edc29f2bce4da2ffd8bc"},
|
||||
{file = "llama_index-0.12.13.tar.gz", hash = "sha256:1e39a397dcc51dabe280c121fd8d5451a6a84595233a8b26caa54d9b7ecf9ffc"},
|
||||
{file = "llama_index-0.12.12-py3-none-any.whl", hash = "sha256:208f77dba5fd8268cacd3d56ec3ee33b0001d5b6ec623c5b91c755af7b08cfae"},
|
||||
{file = "llama_index-0.12.12.tar.gz", hash = "sha256:d4e475726e342b1178736ae3ed93336fe114605e86431b6dfcb454a9e1f26e72"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-agent-openai = ">=0.4.0,<0.5.0"
|
||||
llama-index-cli = ">=0.4.0,<0.5.0"
|
||||
llama-index-core = ">=0.12.13,<0.13.0"
|
||||
llama-index-core = ">=0.12.12,<0.13.0"
|
||||
llama-index-embeddings-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-indices-managed-llama-cloud = ">=0.4.0"
|
||||
llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
@@ -4015,13 +4001,13 @@ llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-core"
|
||||
version = "0.12.13"
|
||||
version = "0.12.12"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_core-0.12.13-py3-none-any.whl", hash = "sha256:9708bb594bbddffd6ff0767242e49d8978d1ba60a2e62e071d9d123ad2f17e6f"},
|
||||
{file = "llama_index_core-0.12.13.tar.gz", hash = "sha256:77af0161246ce1de38efc17cb6438dfff9e9558af00bcfac7dd4d0b7325efa4b"},
|
||||
{file = "llama_index_core-0.12.12-py3-none-any.whl", hash = "sha256:cea491e87f65e6b775b5aef95720de302b85af1bdc67d779c4b09170a30e5b98"},
|
||||
{file = "llama_index_core-0.12.12.tar.gz", hash = "sha256:068b755bbc681731336e822f5977d7608585e8f759c6293ebd812e2659316a37"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -5483,7 +5469,6 @@ description = "Nvidia JIT LTO Library"
|
||||
optional = false
|
||||
python-versions = ">=3"
|
||||
files = [
|
||||
{file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83"},
|
||||
{file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57"},
|
||||
{file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1"},
|
||||
]
|
||||
@@ -5596,17 +5581,15 @@ realtime = ["websockets (>=13,<15)"]
|
||||
|
||||
[[package]]
|
||||
name = "openhands-aci"
|
||||
version = "0.1.9"
|
||||
version = "0.1.8"
|
||||
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.12"
|
||||
files = [
|
||||
{file = "openhands_aci-0.1.9-py3-none-any.whl", hash = "sha256:62af189878db046aa98475a41fa01200efd5ddf1db8a435c38da3d4ad32cb11a"},
|
||||
{file = "openhands_aci-0.1.9.tar.gz", hash = "sha256:690d33d355a3e4111f52861dbb96ff766b5a268202324a87c94ba67b628a63b1"},
|
||||
]
|
||||
python-versions = "^3.12"
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
diskcache = ">=5.6.3,<6.0.0"
|
||||
diskcache = "^5.6.3"
|
||||
flake8 = "*"
|
||||
gitpython = "*"
|
||||
grep-ast = "0.3.3"
|
||||
@@ -5616,7 +5599,13 @@ numpy = "*"
|
||||
pandas = "*"
|
||||
scipy = "*"
|
||||
tree-sitter = "0.21.3"
|
||||
whatthepatch = ">=1.0.6,<2.0.0"
|
||||
whatthepatch = "^1.0.6"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/All-Hands-AI/openhands-aci.git"
|
||||
reference = "fix-find-show-only-hidden-subpaths"
|
||||
resolved_reference = "910e8c470aff0e496bf262bc673c7ee7b4531159"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
@@ -10130,4 +10119,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "fbca4b2ca0fe2d1d3cac46164c0c1eb9e468dc6f6bc7165e9a3d62ea9f25d801"
|
||||
content-hash = "6b74056694bdc84a4583c2f93a5b218f15688827cb59e289eb83331045a1582e"
|
||||
|
||||
+1
-3
@@ -65,7 +65,7 @@ runloop-api-client = "0.13.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
pygithub = "^2.5.0"
|
||||
joblib = "*"
|
||||
openhands-aci = "0.1.9"
|
||||
openhands-aci = "0.1.8"
|
||||
python-socketio = "^5.11.4"
|
||||
redis = "^5.2.0"
|
||||
sse-starlette = "^2.1.3"
|
||||
@@ -101,7 +101,6 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -130,7 +129,6 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
Reference in New Issue
Block a user